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())

    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 = 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
Android Unit Test实践_第1张图片
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!

你可能感兴趣的:(Android Unit Test实践)