前言

上篇《Android单元测试 - 几个重要问题》 讲解了“何解决Android依赖、隔离Native方法、静态方法、RxJava异步转同步”这几个Presenter单元测试中常见问题。如果读者你消化得差不多,就接着看本篇吧。

在日常开发中,数据储存是必不可少的。例如,网络请求到数据,先存本地,下次打开页面,先从本地读取数据显示,再从服务器请求新数据。既然如此重要,对这块代码进行测试,也成为单元测试的重中之重了。

笔者在学会单元测试前,也像大多数人一样,写好了sql代码,运行app,报错了....检查代码,修改,再运行app....这真是效率太低了。有了单元测试做武器后,我写DAO代码轻松了不少,不担心出错,效率也高。

常用的数据储存有:sqlite、SharedPreference、Assets、文件。由于这前三种储取数据方式,都必须依赖android环境,因此要进行单元测试,不能仅仅用junit & mockito了,需要另外的单元测试框架。接下来,笔者介绍如何使用robolectric进行DAO单元测试。

缩写解释:DAO (Data Access Object) 数据访问对象


Robolectric配置

Robolectric官网:http://robolectric.org/

Robolectric配置很简单的。

build.gradle

dependencies {testCompile "org.robolectric:robolectric:3.1.2"
}

然后在测试用例XXTest加上注解:

@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class)
public class XXTest {
}

配置代码是写完了。

不过,别以为这样就完了。Robolectric最麻烦就是下载依赖! 由于我们生活在天朝,下载国外的依赖很慢,笔者即使有了翻墙,效果也一般,可能是https://oss.sonatype.org 服务器比较慢。

笔者已经下载好了依赖包,读者们可以到 http://git.oschina.net/kkmike999/Robolectric-Dependencies 下载robolectric 3.1.2的依赖包,按照Readme.md说明操作。


Sqlite

DbHelper:

public class DbHelper extends SQLiteOpenHelper {private static final int DB_VERSION = 1;public DbHelper(Context context, String dbName) {super(context, dbName, null, DB_VERSION);}...
}

Bean:

public class Bean {int id;String name = "";public Bean(int id, String name) {this.id = id;this.name = name;}
}

Bean数据操作类 BeanDAO:

public class BeanDAO {static boolean isTableExist;SQLiteDatabase db;public BeanDAO() {this.db = new DbHelper(App.getContext(), "Bean").getWritableDatabase();}/*** 插入Bean*/public void insert(Bean bean) {checkTable();ContentValues values = new ContentValues();values.put("id", bean.getId());values.put("name", bean.getName());db.insert("Bean", "", values);}/*** 获取对应id的Bean*/public Bean get(int id) {checkTable();Cursor cursor = null;try {cursor = db.rawQuery("SELECT * FROM Bean", null);if (cursor != null && cursor.moveToNext()) {String name = cursor.getString(cursor.getColumnIndex("name"));return new Bean(id, name);}} catch (Exception e) {e.printStackTrace();} finally {if (cursor != null) {cursor.close();}cursor = null;}return null;}/*** 检查表是否存在,不存在则创建表*/private void checkTable() {if (!isTableExist()) {db.execSQL("CREATE TABLE IF NOT EXISTS Bean ( id INTEGER PRIMARY KEY, name )");}}private boolean isTableExist() {if (isTableExist) {return true; // 上次操作已确定表已存在于数据库,直接返回true}Cursor cursor = null;try {String sql = "SELECT COUNT(*) AS c FROM sqlite_master WHERE type ='table' AND name ='Bean' ";cursor = db.rawQuery(sql, null);if (cursor != null && cursor.moveToNext()) {int count = cursor.getInt(0);if (count > 0) {isTableExist = true; // 记录Table已创建,下次执行isTableExist()时,直接返回truereturn true;}}} catch (Exception e) {e.printStackTrace();} finally {if (cursor != null) {cursor.close();}cursor = null;}return false;}
}

以上是你在项目中用到的类,当然数据库一般开发者都会用第三方库,例如:greenDAO、ormlite、dbflow、afinal、xutils....这里考虑到代码演示规范性、通用性,就直接用android提供的SQLiteDatabase。

大家注意到BeanDAO的构造函数:

public BeanDAO() {this.db = new DbHelper(App.getContext(), "Bean").getWritableDatabase();
}

这种在内部创建对象的方式,不利于单元测试。App是项目本来的Application,但是使用Robolectric往往会指定一个测试专用的Application(命名为RoboApp,配置方法下面会介绍),这么做好处是隔离App的所有依赖。

隔离原Application依赖

项目原本的App:

public class App extends Application {private static Context context;@Overridepublic void onCreate() {super.onCreate();context = this;// 各种第三方初始化,有很多依赖...}public static Context getContext() {return context;}
}

而单元测试使用的RoboApp:

public class RoboApp extends Application {}

如果用Robolectric单元测试,不配置RoboApp,就会调用原来的App,而App有很多第三方库依赖,常见的有static{ Library.load() }静态加载so库。于是,执行App生命周期时,robolectric就报错了。

正确配置Application方式,是在单元测试XXTest加上@Config(application = RoboApp.class)

改进DAO类

public class BeanDAO {SQLiteDatabase db;public BeanDAO(SQLiteDatabase db) {this.db = db;}// 可以保留原来的构造函数,只是单元测试不用这个方法而已public BeanDAO() {this.db = new DbHelper(App.getContext(), "Bean").getWritableDatabase();}

单元测试

DAOTest

@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, manifest = Config.NONE, sdk = Build.VERSION_CODES.JELLY_BEAN, application = RoboApp.class)
public class DAOTest {BeanDAO dao;@Beforepublic void setUp() throws Exception {// 用随机数做数据库名称,让每个测试方法,都用不同数据库,保证数据唯一性DbHelper       dbHelper = new DbHelper(RuntimeEnvironment.application, new Random().nextInt(1000) + ".db");SQLiteDatabase db       = dbHelper.getWritableDatabase();dao = new BeanDAO(db);}@Testpublic void testInsertAndGet() throws Exception {Bean bean = new Bean(1, "键盘男");dao.insert(bean);Bean retBean = dao.get(1);Assert.assertEquals(retBean.getId(), 1);Assert.assertEquals(retBean.getName(), "键盘男");}
}

DAO单元测试跟Presenter有点不一样,可以说会更简单、直观。Presenter单元测试会用mock去隔离一些依赖,并且模拟返回值,但是sqlite执行是真实的,不能mock的。

正常情况,insert()get()应该分别测试,但这样非常麻烦,必然要在测试用例写sqlite语句,并且对SQLiteDatabase 操作。考虑到数据库操作的真实性,笔者把insertget放在同一个测试用例:如果insert()失败,那么get()必然拿不到数据,testInsertAndGet()失败;只有insert()get()代码都正确,testInsertAndGet()才能通过

由于用Robolectric,所以单元测试要比直接junit要慢。仅junit跑单元测试,耗时基本在毫秒(ms)级,而robolectric则是秒级(s)。不过怎么说也比跑真机、模拟器的单元测试要快很多。


SharedPreference

其实,SharedPreference道理跟sqlite一样,也是对每个测试用例创建单独SharedPreference,然后保存、查找一起测。

ShareDAO:

public class ShareDAO {SharedPreferences        sharedPref;SharedPreferences.Editor editor;public ShareDAO(SharedPreferences sharedPref) {this.sharedPref = sharedPref;this.editor = sharedPref.edit();}public ShareDAO() {this(App.getContext().getSharedPreferences("myShare", Context.MODE_PRIVATE));}public void put(String key, String value) {editor.putString(key, value);editor.apply();}public String get(String key) {return sharedPref.getString(key, "");}
}

单元测试ShareDAOTest

@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, manifest = Config.NONE, sdk = Build.VERSION_CODES.JELLY_BEAN, application = RoboApp.class)
public class ShareDAOTest {ShareDAO shareDAO;@Beforepublic void setUp() throws Exception {String name = new Random().nextInt(1000) + ".pref";shareDAO = new ShareDAO(RuntimeEnvironment.application.getSharedPreferences(name, Context.MODE_PRIVATE));}@Testpublic void testPutAndGet() throws Exception {shareDAO.put("key01", "stringA");String value = shareDAO.get("key01");Assert.assertEquals(value, "stringA");}
}

测试通过了。是不是很简单?


Assets

Robolectric对Assets支持也是相当不错的,测Assets道理也是跟sqlite、sharePreference相同。

/assets/test.txt:

success
public class AssetsReader {AssetManager assetManager;public AssetsReader(AssetManager assetManager) {this.assetManager = assetManager;}public AssetsReader() {assetManager = App.getContext().getAssets();}public String read(String fileName) {try {InputStream inputStream = assetManager.open(fileName);StringBuilder sb = new StringBuilder();byte[] buffer = new byte[1024];int hasRead;while ((hasRead = inputStream.read(buffer, 0, buffer.length)) > -1) {sb.append(new String(buffer, 0, hasRead));}inputStream.close();return sb.toString();} catch (IOException e) {e.printStackTrace();}return "";}
}

单元测试AssetsReaderTest:

@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, manifest = Config.NONE, sdk = Build.VERSION_CODES.JELLY_BEAN, application = RoboApp.class)
public class AssetsReaderTest {AssetsReader assetsReader;@Beforepublic void setUp() throws Exception {assetsReader = new AssetsReader(RuntimeEnvironment.application.getAssets());}@Testpublic void testRead() throws Exception {String value = assetsReader.read("test.txt");Assert.assertEquals(value, "success");}
}

通过了通过了,非常简单!


文件操作

日常开发中,文件操作相对比较少。由于通常都在真机测试,有时目录、文件名有误导致程序出错,还是挺烦人的。所以,笔者教大家在本地做文件操作单元测试。

Environment.getExternalStorageDirectory()

APP运行时,通过Environment.getExternalStorageDirectory()等方法获取android储存目录,因此,只要我们改变Environment.getExternalStorageDirectory()返回的目录,就可以在单元测试时,让jvm写操作指向本地目录。

在《Android单元测试 - 几个重要问题》 介绍过如何解决android.text.TextUtils依赖,那么android.os.Environment也是故伎重演:

test/java目录下,创建android/os/Environment.java

package android.os;public class Environment {public static File getExternalStorageDirectory() {return new File("build");// 返回src/build目录}
}

Context.getCacheDir()

如果你是用contexnt.getCacheDir()getFilesDir()等,那么只需要使用RuntimeEnvironment.application就行。

代码

写完android.os.Environment,我们离成功只差一小步了。FileDAO:

public class FileDAO {Context context;public FileDAO(Context context) {this.context = context;}public void write(String name, String content) {File file = new File(getDirectory(), name);if (!file.getParentFile().exists()) {file.getParentFile().mkdirs();}try {FileWriter fileWriter = new FileWriter(file);fileWriter.write(content);fileWriter.flush();fileWriter.close();} catch (IOException e) {e.printStackTrace();}}public String read(String name) {File file = new File(getDirectory(), name);if (!file.exists()) {return "";}try {FileReader reader = new FileReader(file);StringBuilder sb = new StringBuilder();char[] buffer = new char[1024];int    hasRead;while ((hasRead = reader.read(buffer, 0, buffer.length)) > -1) {sb.append(new String(buffer, 0, hasRead));}reader.close();return sb.toString();} catch (IOException e) {e.printStackTrace();}return "";}public void delete(String name) {File file = new File(getDirectory(), name);if (file.exists()) {file.delete();}}protected File getDirectory() {// return context.getCacheDir();return Environment.getExternalStorageDirectory();}
}

FileDAO单元测试

@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, manifest = Config.NONE, sdk = Build.VERSION_CODES.JELLY_BEAN, application = RoboApp.class)
public class FileDAOTest {FileDAO fileDAO;@Beforepublic void setUp() throws Exception {fileDAO = new FileDAO(RuntimeEnvironment.application);}@Testpublic void testWrite() throws Exception {String name = "readme.md";fileDAO.write(name, "success");String content = fileDAO.read(name);Assert.assertEquals(content, "success");// 一定要删除测试文件,保留的文件会影响下次单元测试fileDAO.delete(name);}
}

注意,用Environment.getExternalStorageDirectory()是不需要robolectric的,直接junit即可;而context.getCacheDir()需要robolectric。


小技巧

如果你嫌麻烦每次都要写@RunWith(RobolectricTestRunner.class)&@Config(...),那么可以写一个基类:

@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, manifest = Config.NONE, sdk = Build.VERSION_CODES.JELLY_BEAN, application = RoboApp.class)
public class RoboCase {protected Context getContext() {return RuntimeEnvironment.application;}
}

然后,所有使用robolectric的测试用例,直接继承RoboCase即可。


小结

我想,大家应该感觉到,Sqlite、SharedPreference、Assets、文件操作几种单元测试,形式都差不多。有这种感觉就对了,举一反三。

本篇文字描述不多,代码比例较大,相信读者能看懂的。

如果读者对Presenter、DAO单元测试运用自如,那应该跟笔者水平相当了,哈哈哈。下一篇会介绍如何优雅地测试传参对象,敬请期待!


关于作者

我是键盘男。
在广州生活,在创业公司上班,猥琐文艺码农。喜欢科学、历史,玩玩投资,偶尔独自旅行。

Android单元测试 - Sqlite、SharedPreference、Assets、文件操作 怎么测?相关推荐

  1. android raw文件作用,Android 中raw和assets文件夹的区别

    Android 中raw和assets文件夹的区别 发布时间:2020-09-25 08:40:41 来源:脚本之家 阅读:103 作者:lqh Android 中raw和assets文件夹的区别 以 ...

  2. Android开发 SQLite 通过.db文件导入已有数据库

    见过几次Android数据库操作,貌似都是在程序开始时建一个空数据库,然后进行操作. 那,如果想要用一个已有的数据库怎么办? 因为Android系统下的数据库是存放在/data/data/com.*. ...

  3. Android开发_raw和assets文件夹的区别

    raw和assets的相同点: 1.两者目录下的文件在打包后会原封不动的保存在apk包中,不会被编译成二进制. raw和assets的不同点: 1.res/raw中的文件会被映射到R.java文件中, ...

  4. Android基础篇 访问Assets文件夹里面的资源【文本、图片、音频、字体包】

    一.创建Assest文件夹 直接把资源复制粘贴到该文件夹下 (1)获取Assets文件夹的管理类 AssetManager assets = getAssets(); (2)遍历文件夹下的资源列表 S ...

  5. Android数据库:SQLite除了.db文件,还多出.db-shm,.db-wal文件

    使用安卓数据库保存文件时,在Android Studio 的 Device File Explorer里数据库文件目录 /data/data/com.urovo.datatopc/databases/ ...

  6. Android第五次课→文件操作

    一个简单的登陆操作,是否要保存密码... MainActivity.java文件 package com.jky.file; import java.io.ByteArrayOutputStream; ...

  7. Android文件读写操作(assets 文件、 raw文件、内部存储文件、外部存储文件)

    Android中的文件读写操作是不可或缺的,每个应用都会涉及到读写操作.这里将读写操作分成了四个部分 assets文件夹中文件数据的读取 raw文件夹中的文件数据的读取 Android内部存储文件的读 ...

  8. android文件读取工具类,Android 下读取Assets Properties操作封装工具类

    Android 下读取Assets Properties操作封装工具类 发布时间:2018-06-03作者:laosun阅读(2081) 为了方便使用,首先创建BaseApplication类,如下所 ...

  9. AndroidStudio_android中实现对properties文件的读写操作_不把properties文件放在assets文件夹中_支持读写---Android原生开发工作笔记238

    这个东西还挺麻烦,因为是android中,我们一般把文件放到assets文件夹中去,但是实际上,这个raw文件夹和assets文件夹 是只读的,对,就是只读的只能读取,不能写入,所以一定要把文件写入到 ...

最新文章

  1. 3.依赖注入 spring_di
  2. CF1157G. Inverse of Rows and Columns
  3. 日常小问题汇总(1)
  4. mysql定时发送慢日志到邮件
  5. python动态获取cookie_看到很多人求助python 我也求助一下如何写cookie的获取和登录吧...
  6. linux efi分区安装grub2,编译UEFI版本Grub2引导多系统文件efi
  7. 基于Unique ID的单片机程序加密系统 单片机唯一ID程序加密
  8. JSP技术的学习总结
  9. 计算机在聋校教学中有哪些作用,现代信息技术在聋校语文教学中的应用
  10. 1.1.6 LSDB同步
  11. DOS下Debug工具使用
  12. 计算机机房一般在几楼,设备层一般在高层楼房第几层?
  13. EOJ#3369. 三千米健身步道
  14. Mysql Yum安装
  15. 字节社招经历:5年Java开发经验,半月3次面试,成功拿到 Offer
  16. 【佳学基因人工智能】ANACONDA下安装SCIPY
  17. 资讯_邮件基本常识普及(to/cc/bcc) ;
  18. String的属性和方法实例 Dart
  19. html 加载pdf文件内容不显示不出来,pdf.js首次加载pdf文件时找不到pdf文件,刷新后才能出现pdf文件...
  20. 小白入门SQL基础知识汇总

热门文章

  1. 进程的一生@unix
  2. /a.out , nohut ./a.out , nohup ./a.out 的区别
  3. 测试用例-写测试用例时怎么入手
  4. opencv python3 文本区域识别_使用等高线从图像中提取文本区域 - Opencv,Python
  5. jquery和css的区别是什么?
  6. 【C语言】牛客网编程初学者入门训练-BC88-BC98
  7. 畅易阁老是显示服务器忙,畅易阁全服开放 盘点天龙玩家卖号的几大原因
  8. java 全局返回码设计_服务返回码的设计
  9. php代码正确 插不进表,在表中插入值在PHP中不工作,使用
  10. python实现选择文件_用tkinter 实现从文件夹选择文件并显示