Android|Android Unit Test实践

为什么Android Unit Test在项目团队中没有普遍应用,主要原因还是Android Api的调用依赖设备,另外一部分是除了ui代码外纯逻辑的代码不多,这篇文章主要针对困难,提供其解决方案,方便大家在项目中用起Unit Test。
Android Unit Test的常见问题

  • 异步任务执行测试;
  • 项目代码解偶不彻底,某方法的边界很多或不好在真实场景下创造;
  • 静态方法不好mock;
  • Kotlin中的类和方法没有默认open,无法mock
  • Kotlin中只读变量无法mock;
  • Adnroid项目中有很多和设备相关的Api,比如Context,Environment等等。
下面针对这些问题一一分析。
Android Unit Test ”Hello World"
  • junit configure
    dependencies junit aar in gradle:
testImplementation 'junit:junit:4.12'

  • 创建单元测试类
    junit test目录在src目录下(即与main在同一目录),名字为test,如果没有可以手动创建目录。
    创建对应类的Junit test类,在类代码中,在File文件中选中被测类名,右击 -> Generate -> Test,填写类名和勾选测试方法即可,点击Ok,会提示选test还是AndroidTest,选test点OK,Android Studio会在test对应目录下创建Test类。
  • 代码:被测类UnitTestHelloWorld.kt
class UnitTestHelloWorld { fun add(a: Int, b: Int): Int { returna + b } }

  • 代码:测试类UnitTestHelloWorldTest.kt
class UnitTestHelloWorldTest { // 这个方法有个注解,表示一个Unit Test @Test fun add() { val result = UnitTestHelloWorld().add(10, 10) assertEquals(20, result) } }

运行Unit Test 两种方式运行:
一. 批量运行test,右击左边Project栏下对应的类文件或对应包名,选中类名会运行该类所有test,选中包名会运行包下面所有类的test,右击后选择"Run "Tests in xxxx""即可,在Run View中可以看到Test运行结果和输出。
二. 运行单个test,在Test类文件中左边行号附近有个运行按钮,点击即可运行单个test。
运行结果会在Run窗口中显示,信息包括运行了多少个test,多少个通过,多少个不通过,不通过的是哪些。
Debug Unit Test 在对应的代码中添加断点,和运行操作一样,运行弹窗选择中的Debug即可。
异步任务执行测试
对异步任务执行进行测试时,如果单元测试方法中不做处理,单元测试会一直执行到方法底部而结束,并不会等待异步任务执行完,处理异步等待的一个比较好的方式是通过CountDownLatch类来执行等待,该类不仅可以等待,还可以设置等待的任务数量。
线程池异步执行类:
class SingleThreadAsyncHelper private constructor(){ companion object { val sInstance: SingleThreadAsyncHelper by lazy (mode = LazyThreadSafetyMode.SYNCHRONIZED) { SingleThreadAsyncHelper() } } private val mExecutor: ThreadPoolExecutor = ThreadPoolExecutor(1, 1, 10 * 60, TimeUnit.SECONDS, LinkedBlockingQueue())init { mExecutor.allowCoreThreadTimeOut(true) }fun submitTask(taskAction: () -> T): Future { return mExecutor.submit(Callable { taskAction.invoke() }) } }

Test类:
class SingleThreadAsyncHelperTest { @Test fun submitTask() { // 异步同步信号,设置等待的信号数量为1 val signal = CountDownLatch(1) var value = https://www.it610.com/article/0 // 异步执行,与测试线程不是一个 SingleThreadAsyncHelper.sInstance.submitTask { Thread.sleep(2000) value++ // 减少等待的信号数量 signal.countDown() } // 线程等待,直到信号量为0 signal.await() // 得到测试结果 assertEquals(1, value) } }

项目代码解偶不彻底,某方法的边界很多或不好在真实场景下创造
当然可测性是代码设计的一个重要参考项,但是无论项目设计多好都会有依赖,某些依赖或复杂场景无法显示创造,我们可以对一些依赖和一些复杂场景进行模拟,设置任何我们想要的场景,我们采用Mockito库,下面对一个提交很对文件的任务进行测试来介绍Mockito,注意一下的Test不能直接运行。
  • 引用Mockito库的依赖
testImplementation "org.mockito:mockito-core:2.23.0"

class FinishTaskTest { private val questionStatus = QuestionSetStatus()@get:Rule public var rule = PowerMockRule()@Before fun setUp() { val baseUrl = MockRetrofit.BASE_URL_SUBMIT_FINISH // 1--这部分后面再讲 PowerMockito.mockStatic(Env::class.java) // 这是mockEnv.getBaseUrl()的返回值为我们自定义的地址 Mockito.`when`(Env.getBaseUrl()).thenReturn(baseUrl) // 2--这部分后面再讲 PowerMockito.mockStatic(APIService::class.java) // 这是mockRetrofit请求类,MockRetrofit里面我们自己根据url自定义了返回结果Mockito.`when`(APIService.createRxService(HomeworkApi::class.java)).thenReturn( MockRetrofit.getMockService( HomeworkApi::class.java, baseUrl))}@Test fun getTask() { val finishTask = FinishTask(0.8f, 0.2f) // 这是异步等待接口提交 val disposableAndProgress = doFinishTaskAwait(finishTask) assertEquals(100, disposableAndProgress.second) } }

Mockito使用比较简单,其他api使用和实现原理可以参考Mockito官网和Mockito源码。
静态方法不好mock
上面提的Mockito库是无法mock静态方法的,如果要mock静态方法,我们可以使用PowerMockito。
  • 引入PowerMockito lib
testImplementation "org.powermock:powermock-module-junit4:1.6.6" testImplementation "org.powermock:powermock-module-junit4-rule:1.6.6" testImplementation "org.powermock:powermock-api-mockito:1.6.6" testImplementation "org.powermock:powermock-classloading-xstream:1.6.6"

  • Mock Static方法,直接用前面的网络请求的mock案例分析
@RunWith(PowerMockRunner::class) // 设置Runner @PrepareForTest(ApiService::class, APIService::class,// 设置需要mock static的类 Env::class) class SubjectiveALiYunAllFileTaskTest { // 1.实践发现还需要加这一行 @get:Rule public var rule = PowerMockRule()@Before fun setUp() { val baseUrl = MockRetrofit.BASE_URL_SUBMIT_FINISH // 2.对static方法进行mock,只有经过这行,下面的mock才有效 PowerMockito.mockStatic(Env::class.java) Mockito.`when`(Env.getBaseUrl()).thenReturn(baseUrl) // 3.对static方法进行mock,只有经过这行,下面的mock才有效 PowerMockito.mockStatic(APIService::class.java) Mockito.`when`(APIService.createRxService(HomeworkApi::class.java)).thenReturn( MockRetrofit.getMockService( HomeworkApi::class.java, baseUrl))}@Test fun getTask() { val finishTask = FinishTask(0.8f, 0.2f) // 4.这是异步等待接口提交 val disposableAndProgress = doFinishTaskAwait(finishTask) assertEquals(100, disposableAndProgress.second) } }

PowerMockito的其他使用请自我查看文档PowerMockito源码和文档
Kotlin中的类和方法没有默认open,无法mock
默认情况下Mocktio对于final的类和方法不能mock,而Kotlin如果没有添加open修饰默认是final的,这样就会出现很多类和方法是final的,解决该问题是添加一个Mocktio的配置,操作如下:
  • 在添加配置文件test/resources/mockito-extensions/org.mockito.plugins.MockMaker文件,在文件中添加:
mock-maker-inline

Android|Android Unit Test实践
文章图片
image.png
  • Mocktio版本使用2.0以上
私有变量或Kotlin中只读变量无法mock
对于这种情况可以采用反射的方式实现。
上案例:
数据库操作类AsyncAndOrderHomeworkDbManager:
class AsyncAndOrderHomeworkDbManager private constructor(){ companion object { val sInstance: AsyncAndOrderHomeworkDbManager by lazy (mode = LazyThreadSafetyMode.SYNCHRONIZED) { AsyncAndOrderHomeworkDbManager() }/** * 初始化数据库 */ fun initDB(context: Context) { QuestionDatabaseHelper.initDB(context.applicationContext) } }// 1.需要mock以下两个变量 @VisibleForTesting private val mQuestionSetStatusDao: QuestionSetStatusDao = QuestionDatabaseHelper.getQuestionSetDao() @VisibleForTesting private val mQuestionAnswerDao = QuestionDatabaseHelper.getQuestionAnswerDao() }

实现的反射类ReflectionTestUtils:
object ReflectionTestUtils { @Throws(Exception::class) fun setField(objectBean: Any, propertyName: String, newValue: Any?) { //获得ReflectPoint类中的一个属性str1 val field = objectBean.javaClass.getDeclaredField(propertyName) //强制获取属性中的值(私有属性不能轻易获取其值) field.isAccessible = true System.out.println(field.get(objectBean)) //修改属性的值 field.set(objectBean, newValue) } }

测试类:
@RunWith(RobolectricTestRunner::class) @PowerMockIgnore("org.mockito.*", "org.robolectric.*", "android.*") @PrepareForTest(AsyncAndOrderHomeworkDbManager::class) class AsyncAndOrderHomeworkDbManagerTest { @Before fun setUp() { // 1.反射修改私有变量 ReflectionTestUtils.setField(AsyncAndOrderHomeworkDbManager.sInstance, "mQuestionSetStatusDao", QuestionDatabaseHelper.getQuestionSetDao()) ReflectionTestUtils.setField(AsyncAndOrderHomeworkDbManager.sInstance, "mQuestionAnswerDao", QuestionDatabaseHelper.getQuestionAnswerDao()) } }

当然像这种反射工具类和上面的RetrofitMock类MockRetrofit可以在平常的实践中慢慢积累,之后遇到类似工具类可以直接用。
Adnroid项目中有很多和设备相关的Api,比如Context,Environment等等,导致很多地方无法运行单元测试
Android项目中对设备的依赖就是因为android.jar,开发引用的android.jar中的实现很多都是throw RuntimeException,具体实现会在app安装到设备上时,使用设备上的android.jar。Robolectric正是在这种环境下诞生的开源Android单元测试框架。Robolectric自己实现了Android启动的相关库,例如Application、Acticity等,我们可以通过activityController.create()来启动一个activity,除此之外还有文件系统等。
  • 引入Robolectric lib
testImplementation 'org.robolectric:robolectric:3.0'

  • 在Test中使用,已测试数据库读写为案例
@RunWith(RobolectricTestRunner::class) // 1.配置Runner @PowerMockIgnore("org.mockito.*", "org.robolectric.*", "android.*")// 2.这是PowerMock和Robolectric冲突的点 @PrepareForTest(AsyncAndOrderHomeworkDbManager::class) class AsyncAndOrderHomeworkDbManagerTest { private val questionSetStatus = QuestionSetStatus().apply { questionSetId = 1 questionSetType = 1 uid = 1 name = "questionSetStatus" }@Before fun setUp() { // 3.初始化数据库,这里的RuntimeEnvironment是Robolectric提供 QuestionDatabaseHelper.initDB(RuntimeEnvironment.application) // 4.应用新的数据库对象 // 5.反射修改对数据库引用的property,因为每执行一个test开始时都会调用下@Before[setUp()]和执行结束时都会调用@After[tearDown], // 6.所以避免数据库被重复打开需要结束时关闭以下,同时单例中引用的数据库对象也需要改变。ReflectionTestUtils.setField(AsyncAndOrderHomeworkDbManager.sInstance, "mQuestionSetStatusDao", QuestionDatabaseHelper.getQuestionSetDao()) ReflectionTestUtils.setField(AsyncAndOrderHomeworkDbManager.sInstance, "mQuestionAnswerDao", QuestionDatabaseHelper.getQuestionAnswerDao()) }@After fun tearDown() { // 7.一个test结束,关闭数据库对象 QuestionDatabaseHelper.getDB().close() }@Test fun asyncGetQuestionSet() { // Test处理异步的测试 val signal = CountDownLatch(1)// 写数据库 AsyncAndOrderHomeworkDbManager.sInstance.asyncSaveOrUpdateQuestionSetWait(questionSetStatus) var getQuestionSetStatus: QuestionSetStatus? = null // 读数据库 AsyncAndOrderHomeworkDbManager.sInstance.asyncGetQuestionSet(1, 1, 1) .subscribeOn(Schedulers.io()) .observeOn(Schedulers.io()) .subscribe ({ // 把异步的执行结果保存 getQuestionSetStatus = it // 通知异步等待结束 signal.countDown() }, { System.out.println(Log.getStackTraceString(it)) signal.countDown() },{ signal.countDown() })// 等待执行完成 signal.await()Assert.assertEquals("questionSetStatus", getQuestionSetStatus?.name) } }

【Android|Android Unit Test实践】End!

    推荐阅读