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

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

[[174579]]

前言

上篇《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 :

  1. dependencies { 
  2.     testCompile "org.robolectric:robolectric:3.1.2" 
  3.  

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

  1. @RunWith(RobolectricTestRunner.class) 
  2. @Config(constants = BuildConfig.class) 
  3. public class XXTest { 
  4.  

配置代码是写完了。

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

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

Sqlite

DbHelper:

  1. public class DbHelper extends SQLiteOpenHelper { 
  2.  
  3.     private static final int DB_VERSION = 1; 
  4.  
  5.     public DbHelper(Context context, String dbName) { 
  6.         super(context, dbName, null, DB_VERSION); 
  7.     } 
  8.     ... 
  9.  

Bean:

  1. public class Bean { 
  2.     int id; 
  3.     String name = ""
  4.  
  5.     public Bean(int id, String name) { 
  6.         this.id = id; 
  7.         this.name = name
  8.     } 
  9.  

Bean数据操作类 BeanDAO:

  1. public class BeanDAO { 
  2.     static boolean isTableExist; 
  3.      
  4.     SQLiteDatabase db; 
  5.  
  6.     public BeanDAO() { 
  7.         this.db = new DbHelper(App.getContext(), "Bean").getWritableDatabase(); 
  8.     } 
  9.  
  10.     /** 
  11.      * 插入Bean 
  12.      */ 
  13.     public void insert(Bean bean) { 
  14.         checkTable(); 
  15.  
  16.         ContentValues values = new ContentValues(); 
  17.         values.put("id", bean.getId()); 
  18.         values.put("name", bean.getName()); 
  19.  
  20.         db.insert("Bean"""values); 
  21.     } 
  22.  
  23.     /** 
  24.      * 获取对应id的Bean 
  25.      */ 
  26.     public Bean get(int id) { 
  27.         checkTable(); 
  28.  
  29.         Cursor cursor = null
  30.  
  31.         try { 
  32.             cursor = db.rawQuery("SELECT * FROM Bean"null); 
  33.  
  34.             if (cursor != null && cursor.moveToNext()) { 
  35.                 String name = cursor.getString(cursor.getColumnIndex("name")); 
  36.  
  37.                 return new Bean(id, name); 
  38.             } 
  39.         } catch (Exception e) { 
  40.             e.printStackTrace(); 
  41.         } finally { 
  42.             if (cursor != null) { 
  43.                 cursor.close(); 
  44.             } 
  45.             cursor = null
  46.         } 
  47.         return null
  48.     } 
  49.      
  50.     /** 
  51.      * 检查表是否存在,不存在则创建表 
  52.      */ 
  53.     private void checkTable() { 
  54.         if (!isTableExist()) { 
  55.             db.execSQL("CREATE TABLE IF NOT EXISTS Bean ( id INTEGER PRIMARY KEY, name )"); 
  56.         } 
  57.     } 
  58.  
  59.     private boolean isTableExist() { 
  60.         if (isTableExist) { 
  61.             return true; // 上次操作已确定表已存在于数据库,直接返回true 
  62.         } 
  63.          
  64.         Cursor cursor = null
  65.         try { 
  66.             String sql = "SELECT COUNT(*) AS c FROM sqlite_master WHERE type ='table' AND name ='Bean' "
  67.  
  68.             cursor = db.rawQuery(sql, null); 
  69.             if (cursor != null && cursor.moveToNext()) { 
  70.                 int count = cursor.getInt(0); 
  71.                 if (count > 0) { 
  72.                     isTableExist = true; // 记录Table已创建,下次执行isTableExist()时,直接返回true 
  73.                     return true
  74.                 } 
  75.             } 
  76.         } catch (Exception e) { 
  77.             e.printStackTrace(); 
  78.         } finally { 
  79.             if (cursor != null) { 
  80.                 cursor.close(); 
  81.             } 
  82.             cursor = null
  83.         } 
  84.         return false
  85.     } 
  86.  

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

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

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

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

隔离原Application依赖

项目原本的App:

  1. public class App extends Application { 
  2.  
  3.     private static Context context; 
  4.  
  5.     @Override 
  6.     public void onCreate() { 
  7.         super.onCreate(); 
  8.         context = this; 
  9.          
  10.         // 各种第三方初始化,有很多依赖 
  11.         ... 
  12.     } 
  13.  
  14.     public static Context getContext() { 
  15.         return context; 
  16.     } 
  17.  

而单元测试使用的RoboApp:

  1. public class RoboApp extends Application {} 

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

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

改进DAO类

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

单元测试

DAOTest

  1. @RunWith(RobolectricTestRunner.class) 
  2. @Config(constants = BuildConfig.class, manifest = Config.NONE, sdk = Build.VERSION_CODES.JELLY_BEAN, application = RoboApp.class) 
  3. public class DAOTest { 
  4.  
  5.     BeanDAO dao; 
  6.  
  7.     @Before 
  8.     public void setUp() throws Exception { 
  9.         // 用随机数做数据库名称,让每个测试方法,都用不同数据库,保证数据唯一性 
  10.         DbHelper       dbHelper = new DbHelper(RuntimeEnvironment.application, new Random().nextInt(1000) + ".db"); 
  11.         SQLiteDatabase db       = dbHelper.getWritableDatabase(); 
  12.  
  13.         dao = new BeanDAO(db); 
  14.     } 
  15.  
  16.     @Test 
  17.     public void testInsertAndGet() throws Exception { 
  18.         Bean bean = new Bean(1, "键盘男"); 
  19.  
  20.         dao.insert(bean); 
  21.  
  22.         Bean retBean = dao.get(1); 
  23.  
  24.         Assert.assertEquals(retBean.getId(), 1); 
  25.         Assert.assertEquals(retBean.getName(), "键盘男"); 
  26.     } 
  27.  

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

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

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

SharedPreference

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

ShareDAO:

  1. public class ShareDAO { 
  2.     SharedPreferences        sharedPref; 
  3.     SharedPreferences.Editor editor; 
  4.  
  5.     public ShareDAO(SharedPreferences sharedPref) { 
  6.         this.sharedPref = sharedPref; 
  7.         this.editor = sharedPref.edit(); 
  8.     } 
  9.  
  10.     public ShareDAO() { 
  11.         this(App.getContext().getSharedPreferences("myShare", Context.MODE_PRIVATE)); 
  12.     } 
  13.  
  14.     public void put(String key, String value) { 
  15.         editor.putString(key, value); 
  16.         editor.apply(); 
  17.     } 
  18.  
  19.     public String get(String key) { 
  20.         return sharedPref.getString(key""); 
  21.     } 
  22.  

单元测试ShareDAOTest

  1. @RunWith(RobolectricTestRunner.class) 
  2. @Config(constants = BuildConfig.class, manifest = Config.NONE, sdk = Build.VERSION_CODES.JELLY_BEAN, application = RoboApp.class) 
  3. public class ShareDAOTest { 
  4.  
  5.     ShareDAO shareDAO; 
  6.  
  7.     @Before 
  8.     public void setUp() throws Exception { 
  9.         String name = new Random().nextInt(1000) + ".pref"
  10.  
  11.         shareDAO = new ShareDAO(RuntimeEnvironment.application.getSharedPreferences(name, Context.MODE_PRIVATE)); 
  12.     } 
  13.  
  14.     @Test 
  15.     public void testPutAndGet() throws Exception { 
  16.         shareDAO.put("key01""stringA"); 
  17.  
  18.         String value = shareDAO.get("key01"); 
  19.  
  20.         Assert.assertEquals(value, "stringA"); 
  21.     } 

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

Assets

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

/assets/test.txt: 

  1. success  
  1. public class AssetsReader { 
  2.  
  3.     AssetManager assetManager; 
  4.  
  5.     public AssetsReader(AssetManager assetManager) { 
  6.         this.assetManager = assetManager; 
  7.     } 
  8.  
  9.     public AssetsReader() { 
  10.         assetManager = App.getContext() 
  11.                           .getAssets(); 
  12.     } 
  13.  
  14.     public String read(String fileName) { 
  15.         try { 
  16.             InputStream inputStream = assetManager.open(fileName); 
  17.  
  18.             StringBuilder sb = new StringBuilder(); 
  19.  
  20.             byte[] buffer = new byte[1024]; 
  21.  
  22.             int hasRead; 
  23.  
  24.             while ((hasRead = inputStream.read(buffer, 0, buffer.length)) > -1) { 
  25.                 sb.append(new String(buffer, 0, hasRead)); 
  26.             } 
  27.  
  28.             inputStream.close(); 
  29.  
  30.             return sb.toString(); 
  31.         } catch (IOException e) { 
  32.             e.printStackTrace(); 
  33.         } 
  34.         return ""
  35.     } 
  36.  

单元测试AssetsReaderTest:

  1. @RunWith(RobolectricTestRunner.class) 
  2. @Config(constants = BuildConfig.class, manifest = Config.NONE, sdk = Build.VERSION_CODES.JELLY_BEAN, application = RoboApp.class) 
  3. public class AssetsReaderTest { 
  4.  
  5.     AssetsReader assetsReader; 
  6.  
  7.     @Before 
  8.     public void setUp() throws Exception { 
  9.         assetsReader = new AssetsReader(RuntimeEnvironment.application.getAssets()); 
  10.     } 
  11.  
  12.     @Test 
  13.     public void testRead() throws Exception { 
  14.         String value = assetsReader.read("test.txt"); 
  15.  
  16.         Assert.assertEquals(value, "success"); 
  17.     } 
  18.  

 

 

 

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

文件操作

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

Environment.getExternalStorageDirectory()

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

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

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

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

 Context.getCacheDir()

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

代码

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

  1. public class FileDAO { 
  2.  
  3.     Context context; 
  4.  
  5.     public FileDAO(Context context) { 
  6.         this.context = context; 
  7.     } 
  8.      
  9.     public void write(String name, String content) { 
  10.         File file = new File(getDirectory(), name); 
  11.  
  12.         if (!file.getParentFile().exists()) { 
  13.             file.getParentFile().mkdirs(); 
  14.         } 
  15.         try { 
  16.             FileWriter fileWriter = new FileWriter(file); 
  17.  
  18.             fileWriter.write(content); 
  19.             fileWriter.flush(); 
  20.             fileWriter.close(); 
  21.         } catch (IOException e) { 
  22.             e.printStackTrace(); 
  23.         } 
  24.     } 
  25.  
  26.     public String read(String name) { 
  27.         File file = new File(getDirectory(), name); 
  28.  
  29.         if (!file.exists()) { 
  30.             return ""
  31.         } 
  32.  
  33.         try { 
  34.             FileReader reader = new FileReader(file); 
  35.  
  36.             StringBuilder sb = new StringBuilder(); 
  37.  
  38.             char[] buffer = new char[1024]; 
  39.             int    hasRead; 
  40.  
  41.             while ((hasRead = reader.read(buffer, 0, buffer.length)) > -1) { 
  42.                 sb.append(new String(buffer, 0, hasRead)); 
  43.             } 
  44.             reader.close(); 
  45.  
  46.             return sb.toString(); 
  47.         } catch (IOException e) { 
  48.             e.printStackTrace(); 
  49.         } 
  50.         return ""
  51.     } 
  52.  
  53.     public void delete(String name) { 
  54.         File file = new File(getDirectory(), name); 
  55.  
  56.         if (file.exists()) { 
  57.             file.delete(); 
  58.         } 
  59.     } 
  60.  
  61.     protected File getDirectory() { 
  62.         // return context.getCacheDir(); 
  63.         return Environment.getExternalStorageDirectory(); 
  64.     } 
  65.  

FileDAO单元测试

  1. @RunWith(RobolectricTestRunner.class) 
  2. @Config(constants = BuildConfig.class, manifest = Config.NONE, sdk = Build.VERSION_CODES.JELLY_BEAN, application = RoboApp.class) 
  3. public class FileDAOTest { 
  4.  
  5.     FileDAO fileDAO; 
  6.  
  7.     @Before 
  8.     public void setUp() throws Exception { 
  9.         fileDAO = new FileDAO(RuntimeEnvironment.application); 
  10.     } 
  11.  
  12.     @Test 
  13.     public void testWrite() throws Exception { 
  14.         String name = "readme.md"
  15.  
  16.         fileDAO.write(name"success"); 
  17.  
  18.         String content = fileDAO.read(name); 
  19.  
  20.         Assert.assertEquals(content, "success"); 
  21.  
  22.         // 一定要删除测试文件,保留的文件会影响下次单元测试 
  23.         fileDAO.delete(name); 
  24.     } 
  25.  

 

 

 

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

小技巧

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

  1. @RunWith(RobolectricTestRunner.class) 
  2. @Config(constants = BuildConfig.class, manifest = Config.NONE, sdk = Build.VERSION_CODES.JELLY_BEAN, application = RoboApp.class) 
  3. public class RoboCase { 
  4.  
  5.     protected Context getContext() { 
  6.         return RuntimeEnvironment.application; 
  7.     } 
  8.  

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

小结

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

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

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

关于作者

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

责任编辑:庞桂玉 来源: segmentfault
相关推荐

2017-01-14 23:42:49

单元测试框架软件测试

2010-01-28 15:54:19

Android单元测试

2017-02-21 10:30:17

Android单元测试研究与实践

2011-06-01 15:49:00

Android 测试

2017-01-14 23:26:17

单元测试JUnit测试

2017-01-16 12:12:29

单元测试JUnit

2020-08-18 08:10:02

单元测试Java

2010-02-07 15:42:46

Android单元测试

2017-03-23 16:02:10

Mock技术单元测试

2021-05-05 11:38:40

TestNGPowerMock单元测试

2023-07-26 08:58:45

Golang单元测试

2011-07-04 18:16:42

单元测试

2020-05-07 17:30:49

开发iOS技术

2011-05-16 16:52:09

单元测试彻底测试

2023-12-11 08:25:15

Java框架Android

2016-10-20 12:34:08

android单元测试java

2017-01-14 23:48:18

单元测试编码代码

2013-06-04 09:49:04

Spring单元测试软件测试

2020-09-30 08:08:15

单元测试应用

2009-09-01 10:20:06

protected方法单元测试
点赞
收藏

51CTO技术栈公众号