自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Android單元測試 - Sqlite、SharedPreference、Assets、文件操作 怎么測?

開發(fā) 后端 Android
常用的數(shù)據(jù)儲存有:sqlite、SharedPreference、Assets、文件。由于這前三種儲取數(shù)據(jù)方式,都必須依賴android環(huán)境,因此要進(jìn)行單元測試,不能僅僅用junit & mockito了,需要另外的單元測試框架。接下來,筆者介紹如何使用robolectric進(jìn)行DAO單元測試。

[[174579]]

前言

上篇《Android單元測試 - 幾個(gè)重要問題》 講解了“何解決Android依賴、隔離Native方法、靜態(tài)方法、RxJava異步轉(zhuǎn)同步”這幾個(gè)Presenter單元測試中常見問題。如果讀者你消化得差不多,就接著看本篇吧。

在日常開發(fā)中,數(shù)據(jù)儲存是必不可少的。例如,網(wǎng)絡(luò)請求到數(shù)據(jù),先存本地,下次打開頁面,先從本地讀取數(shù)據(jù)顯示,再從服務(wù)器請求新數(shù)據(jù)。既然如此重要,對這塊代碼進(jìn)行測試,也成為單元測試的重中之重了。

筆者在學(xué)會單元測試前,也像大多數(shù)人一樣,寫好了sql代碼,運(yùn)行app,報(bào)錯(cuò)了....檢查代碼,修改,再運(yùn)行app....這真是效率太低了。有了單元測試做武器后,我寫DAO代碼輕松了不少,不擔(dān)心出錯(cuò),效率也高。

常用的數(shù)據(jù)儲存有:sqlite、SharedPreference、Assets、文件。由于這前三種儲取數(shù)據(jù)方式,都必須依賴android環(huán)境,因此要進(jìn)行單元測試,不能僅僅用junit & mockito了,需要另外的單元測試框架。接下來,筆者介紹如何使用robolectric進(jìn)行DAO單元測試。

縮寫解釋:DAO (Data Access Object) 數(shù)據(jù)訪問對象

Robolectric配置

Robolectric官網(wǎng):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 服務(wù)器比較慢。

 筆者已經(jīng)下載好了依賴包,讀者們可以到 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數(shù)據(jù)操作類 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.      * 獲取對應(yīng)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.      * 檢查表是否存在,不存在則創(chuàng)建表 
  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; // 上次操作已確定表已存在于數(shù)據(jù)庫,直接返回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已創(chuàng)建,下次執(zhí)行isTableExist()時(shí),直接返回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.  

以上是你在項(xiàng)目中用到的類,當(dāng)然數(shù)據(jù)庫一般開發(fā)者都會用第三方庫,例如:greenDAO、ormlite、dbflow、afinal、xutils....這里考慮到代碼演示規(guī)范性、通用性,就直接用android提供的SQLiteDatabase。

大家注意到BeanDAO的構(gòu)造函數(shù): 

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

這種在內(nèi)部創(chuàng)建對象的方式,不利于單元測試。App是項(xiàng)目本來的Application,但是使用Robolectric往往會指定一個(gè)測試專用的Application(命名為RoboApp,配置方法下面會介紹),這么做好處是隔離App的所有依賴。

隔離原Application依賴

項(xiàng)目原本的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,就會調(diào)用原來的App,而App有很多第三方庫依賴,常見的有static{ Library.load() }靜態(tài)加載so庫。于是,執(zhí)行App生命周期時(shí),robolectric就報(bào)錯(cuò)了。

正確配置Application方式,是在單元測試XXTest加上@Config(application = RoboApp.class)。

改進(jìn)DAO類

  1. public class BeanDAO { 
  2.     SQLiteDatabase db; 
  3.  
  4.     public BeanDAO(SQLiteDatabase db) { 
  5.         this.db = db; 
  6.     } 
  7.      
  8.     // 可以保留原來的構(gòu)造函數(shù),只是單元測試不用這個(gè)方法而已 
  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.         // 用隨機(jī)數(shù)做數(shù)據(jù)庫名稱,讓每個(gè)測試方法,都用不同數(shù)據(jù)庫,保證數(shù)據(jù)唯一性 
  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有點(diǎn)不一樣,可以說會更簡單、直觀。Presenter單元測試會用mock去隔離一些依賴,并且模擬返回值,但是sqlite執(zhí)行是真實(shí)的,不能mock的。

正常情況,insert()和get()應(yīng)該分別測試,但這樣非常麻煩,必然要在測試用例寫sqlite語句,并且對SQLiteDatabase 操作??紤]到數(shù)據(jù)庫操作的真實(shí)性,筆者把insert和get放在同一個(gè)測試用例:如果insert()失敗,那么get()必然拿不到數(shù)據(jù),testInsertAndGet()失敗;只有insert()和get()代碼都正確,testInsertAndGet()才能通過。

 由于用Robolectric,所以單元測試要比直接junit要慢。僅junit跑單元測試,耗時(shí)基本在毫秒(ms)級,而robolectric則是秒級(s)。不過怎么說也比跑真機(jī)、模擬器的單元測試要快很多。

SharedPreference

其實(shí),SharedPreference道理跟sqlite一樣,也是對每個(gè)測試用例創(chuàng)建單獨(dú)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支持也是相當(dāng)不錯(cuò)的,測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.  

 

 

 

 通過了通過了,非常簡單!

文件操作

日常開發(fā)中,文件操作相對比較少。由于通常都在真機(jī)測試,有時(shí)目錄、文件名有誤導(dǎo)致程序出錯(cuò),還是挺煩人的。所以,筆者教大家在本地做文件操作單元測試。

Environment.getExternalStorageDirectory()

APP運(yùn)行時(shí),通過Environment.getExternalStorageDirectory()等方法獲取android儲存目錄,因此,只要我們改變Environment.getExternalStorageDirectory()返回的目錄,就可以在單元測試時(shí),讓jvm寫操作指向本地目錄。

在《Android單元測試 - 幾個(gè)重要問題》 介紹過如何解決android.text.TextUtils依賴,那么android.os.Environment也是故伎重演:

在test/java目錄下,創(chuàng)建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.         // 一定要?jiǎng)h除測試文件,保留的文件會影響下次單元測試 
  23.         fileDAO.delete(name); 
  24.     } 
  25.  

 

 

 

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

小技巧

如果你嫌麻煩每次都要寫@RunWith(RobolectricTestRunner.class)&@Config(...),那么可以寫一個(gè)基類:

  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即可。

小結(jié)

我想,大家應(yīng)該感覺到,Sqlite、SharedPreference、Assets、文件操作幾種單元測試,形式都差不多。有這種感覺就對了,舉一反三。

本篇文字描述不多,代碼比例較大,相信讀者能看懂的。

如果讀者對Presenter、DAO單元測試運(yùn)用自如,那應(yīng)該跟筆者水平相當(dāng)了,哈哈哈。下一篇會介紹如何優(yōu)雅地測試傳參對象,敬請期待!

關(guān)于作者

我是鍵盤男。在廣州生活,在創(chuàng)業(yè)公司上班,猥瑣文藝碼農(nóng)。喜歡科學(xué)、歷史,玩玩投資,偶爾獨(dú)自旅行。

責(zé)任編輯:龐桂玉 來源: segmentfault
相關(guān)推薦

2017-01-14 23:42:49

單元測試框架軟件測試

2010-01-28 15:54:19

Android單元測試

2017-02-21 10:30:17

Android單元測試研究與實(shí)踐

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技術(shù)單元測試

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

開發(fā)iOS技術(shù)

2011-05-16 16:52:09

單元測試徹底測試

2016-10-20 12:34:08

android單元測試java

2023-12-11 08:25:15

Java框架Android

2017-01-14 23:48:18

單元測試編碼代碼

2011-04-18 13:20:40

單元測試軟件測試

2017-02-23 15:59:53

測試MockSetup

2013-06-04 09:49:04

Spring單元測試軟件測試
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號