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 {
return a + 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<Runnable>())
init {
mExecutor.allowCoreThreadTimeOut(true)
}
fun <T> submitTask(taskAction: () -> T): Future<T> {
return mExecutor.submit(Callable { taskAction.invoke() })
}
}
Test类:
class SingleThreadAsyncHelperTest {
@Test
fun submitTask() {
// 异步同步信号,设置等待的信号数量为1
val signal = CountDownLatch(1)
var value = 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)
// 这是mock Env.getBaseUrl()的返回值为我们自定义的地址
Mockito.`when`(Env.getBaseUrl()).thenReturn(baseUrl)
// 2--这部分后面再讲
PowerMockito.mockStatic(APIService::class.java)
// 这是mock Retrofit请求类,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
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)
}
}
End!