Android单元测试 - Sqlite、SharedPreference、Assets、文件操作 怎么测? - 24小时 - 剑客 -「科技引领未来」

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

前言 上篇 《Android单元测试 - 几个重要问题》 讲解了“何解决Android依赖、隔离Native方法、静态方法、RxJava异步转同步

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

前言

上篇 《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 服务器比较慢。

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

笔者已经下载好了依赖包,读者们可以到 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()时,直接返回true

return 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;

@Override

public 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;

@Before

public void setUp() throws Exception {

// 用随机数做数据库名称,让每个测试方法,都用不同数据库,保证数据唯一性

DbHelper dbHelper = new DbHelper(RuntimeEnvironment.application, new Random().nextInt(1000) + ".db");

SQLiteDatabase db = dbHelper.getWritableDatabase();

dao = new BeanDAO(db);

}

@Test

public 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() 才能通过

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

由于用 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;

@Before

public void setUp() throws Exception {

String name = new Random().nextInt(1000) + ".pref";

shareDAO = new ShareDAO(RuntimeEnvironment.application.getSharedPreferences(name, Context.MODE_PRIVATE));

}

@Test

public void testPutAndGet() throws Exception {

shareDAO.put("key01", "stringA");

String value = shareDAO.get("key01");

Assert.assertEquals(value, "stringA");

}

}

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

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

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;

@Before

public void setUp() throws Exception {

assetsReader = new AssetsReader(RuntimeEnvironment.application.getAssets());

}

@Test

public void testRead() throws Exception {

String value = assetsReader.read("test.txt");

Assert.assertEquals(value, "success");

}

}

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

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

文件操作

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

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目录

}

}

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

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;

@Before

public void setUp() throws Exception {

fileDAO = new FileDAO(RuntimeEnvironment.application);

}

@Test

public void testWrite() throws Exception {

String name = "readme.md";

fileDAO.write(name, "success");

String content = fileDAO.read(name);

Assert.assertEquals(content, "success");

// 一定要删除测试文件,保留的文件会影响下次单元测试

fileDAO.delete(name);

}

}

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

注意,用 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单元测试运用自如,那应该跟笔者水平相当了,哈哈哈。下一篇会介绍如何优雅地测试传参对象,敬请期待!

关于作者

我是键盘男。

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

未登录用户
全部评论0
到底啦