美文网首页Unit Test
Android Unit Test实践

Android Unit Test实践

作者: 土人徐 | 来源:发表于2019-08-23 17:09 被阅读0次

    为什么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!

    相关文章

      网友评论

        本文标题:Android Unit Test实践

        本文链接:https://www.haomeiwen.com/subject/zxfosctx.html