Jetpack系列(十) — 测试 + 总结

Jetpack系列(十) — 测试 + 总结

开头的话

这是我写的第十篇关于Jetpack的文章,内容相对比较简单,基本上都是一些相关控件的基本使用,但是本着先使用后理解的原则也算是把大部分Jetpack的知识点过了一遍,至于更加深入的部分希望在接下来的学习当中逐步地学习。当然也有一些Jetpack知识点没有放在系列文章当中,比如DataStoreStarUp等,其中Jetpack Compose内容较多,应该后面单独写一个系列,其他的也希望在后续文章当中有所体现。作为本系文章的最后一篇,希望整理一下测试相关的知识点。

本文中重点是记录Jetpack相关的测试,包括RoomViewModelLiveDataHilt等,至于相关的测试库比如Espresso Mockito等,希望可以在后面具体的项目当中有所体现。

测试相关

典型的Android Studio项目包含两个用于放置测试的目录:

  • androidTest 目录应包含在真实或虚拟设备上运行的测试。此类测试包括集成测试、端到端测试,以及仅靠 JVM 无法完成应用功能验证的其他测试。
  • test 目录应包含在本地计算机上运行的测试,如单元测试。

测试金字塔说明了应用应如何包含三类测试

  • 小型测试是指单元测试,用于验证应用的行为,一次验证一个类。
  • 中型测试是指集成测试,用于验证模块内堆栈级别之间的互动或相关模块之间的互动。
  • 大型测试是指端到端测试,用于验证跨越了应用的多个模块的用户操作流程。

pyramid.png

基本使用

简单单元测试

  1. 鼠标放在对应的方法上,右键Generata..,选择Test...,自动生成Test类

    class BaseUtilsTest {
        @Test
        fun add() {
            assertEquals(4, BaseUtils.add(2, 2))
        }
    }
    复制代码
  2. 我这里遇到一个小坑,就是一直包class not find,这里要把before launch 加上
    image-20210517165555709.png

  3. Hamcrest 语义化

    // 导入 Hamcrest   
    // testImplementation 'org.hamcrest:hamcrest-all:1.3'
    
    class StatisticsUtilsKtTest : TestCase() {
    
        @Test
        fun testGetActiveAndCompletedStats() {
            val tasks = listOf<Task>(
                Task("title", "desc", isCompleted = false)
            )
            val result = getActiveAndCompletedStats(tasks)
            assertThat(result.completedTasksPercent, `is`(0f))
            assertThat(result.activeTasksPercent, `is`(100f))
        }
    }
    复制代码

Room 测试

  1. androidTest 文件夹下新建测试类
    @RunWith(AndroidJUnit4::class)
    class WordDaoTest {
        private lateinit var database: AppDataBase
        private lateinit var wordDao: WordDao
    
        private val word1 = Word("hello")
        private val word2 = Word("world")
    
        @get:Rule
        var instantTaskExecutorRule = InstantTaskExecutorRule()
    
        @Before
        fun createDb() = runBlocking {
            val context = InstrumentationRegistry.getInstrumentation().targetContext
            database = Room.inMemoryDatabaseBuilder(context, AppDataBase::class.java).build()
            wordDao = database.wordDao()
    
            wordDao.insertAll(listOf(word1, word2))
        }
    
        @After
        fun closeDb() {
            database.close()
        }
    
        @Test
        fun testGetAlphabetizedWords() = runBlocking {
            val wordList = wordDao.getAlphabetizedWords().first()
            Assert.assertThat(wordList.size, CoreMatchers.equalTo(2))
    
            Assert.assertThat(wordList[0], CoreMatchers.equalTo(word1))
            Assert.assertThat(wordList[1], CoreMatchers.equalTo(word2))
        }
    
        @Test
        fun testDeleteAll() = runBlocking {
            wordDao.deleteAll()
    
            val wordList = wordDao.getAlphabetizedWords().first()
            Assert.assertThat(wordList.size, CoreMatchers.equalTo(0))
        }
    }
    
    复制代码

ViewModel 测试

  1. ViewModel 没有参数时,可以直接实例化,ViewModel 有参数时可以参考Hilt

    @RunWith(AndroidJUnit4::class)
    class HomeViewModelTest {
    
        private lateinit var viewModel: HomeViewModel
    
        private val coroutineRule = MainCoroutineRule()
        private val instantTaskExecutorRule = InstantTaskExecutorRule()
    
        @get:Rule
        val rule = RuleChain
            .outerRule(instantTaskExecutorRule)
            .around(coroutineRule)
    
        @Before
        fun setUp() {
            viewModel = HomeViewModel()
        }
    
        @Test
        fun testDefaultValues() = coroutineRule.runBlockingTest {
            viewModel.updateTaps()
            val value = viewModel.userName.getOrAwaitValue()
            assertThat(value, `is`("aa bb"))
        }
    }
    复制代码
  2. 这里主要是观测LiveData 的变化,所以可用协程的通道来实现数据的观察

    // 官方代码
    @VisibleForTesting(otherwise = VisibleForTesting.NONE)
    fun <T> LiveData<T>.getOrAwaitValue(
        time: Long = 2,
        timeUnit: TimeUnit = TimeUnit.SECONDS,
        afterObserve: () -> Unit = {}
    ): T {
        var data: T? = null
        val latch = CountDownLatch(1)   // 一次计数
        val observer = object : Observer<T> {
            override fun onChanged(o: T?) {
                data = o
                latch.countDown()
                this@getOrAwaitValue.removeObserver(this)
            }
        }
        this.observeForever(observer)
    
        try {
            afterObserve.invoke()
    
            // Don't wait indefinitely if the LiveData is not set.
            if (!latch.await(time, timeUnit)) {
                throw TimeoutException("LiveData value was never set.")
            }
    
        } finally {
            this.removeObserver(observer)
        }
    
        @Suppress("UNCHECKED_CAST")
        return data as T
    }
    复制代码

Hilt 测试

  1. 有一点需要注意的是,Hilt需要提供Application,所以这里可以自定义一个AndroidJUnitRunner,作为测试的启动项

    class MainTestRunner : AndroidJUnitRunner() {
        override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application {
            return super.newApplication(cl, HiltTestApplication::class.java.name, context)
        }
    }
    复制代码
  2. modulebuild.gradle 文件当中修改 testInstrumentationRunner

     defaultConfig {
         ...
    
         testInstrumentationRunner "com.huang.myapplication.MainTestRunner"
    
         ...
     }
    复制代码
  3. @HiltAndroidTest 注解测试类

    @HiltAndroidTest
    class MainViewModelTest {
    
        private lateinit var viewModel: MainViewModel
        private val hiltRule = HiltAndroidRule(this)
        private val instantTaskExecutorRule = InstantTaskExecutorRule()
        private val coroutineRule = MainCoroutineRule()
    
        @get:Rule
        val rule = RuleChain
            .outerRule(hiltRule)
            .around(instantTaskExecutorRule)
            .around(coroutineRule)
    
        @Inject
        lateinit var repo: WordRepository
    
        @Inject
        lateinit var manager: WorkManager
    
        @Before
        fun setUp() {
            hiltRule.inject()
            viewModel = MainViewModel(repo, manager)
        }
    
        @Test
        fun testDefaultValues() = coroutineRule.runBlockingTest {
            val value = viewModel.taps.getOrAwaitValue()
            assertThat(value, `is`("0 taps"))
        }
    }
    复制代码

相关链接

Jetpack系列(一) — Navigation

Jetpack系列(二) — Lifecycle

Jetpack系列(三) — LiveData

Jetpack系列(四) — ViewModel

Jetpack系列(五) — Room

Jetpack系列(六) — Paging3

Jetpack系列(七) — Hilt

Jetpack系列(八) — Data Bidnding && View Binding

Jetpack系列(九) — WorkManager

参考资料

官网

codelab

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享