背景
此文章是对于google code lab中《Introduction to Test Double and Dependence injection》 与 《Testing Basics》的总结,本篇主要讲述如何在mvvm架构的android项目中对Model层以及ViewModel层进行测试
Model层
为什么要测它
model层作为数据获取层,主要与network和数据库打交道,我们需要测试其对数据的获取和更新操作逻辑的正确性
测它的时候会遇到什么问题
如上所述,Model层通常和数据库和网络有较强相关性,我们需要测试的只是其对数据的处理逻辑。
如何解决
改变数据源的获取方式,不要使用内部构造的方式,采用依赖注入方法来进行注入 这是通常写法的Repository代码,里面的dataSource是在内部构建,这就造成了测试的时候难以去除逻辑和数据源的耦合,造成无法进行测试
class DefaultTasksRepository private constructor(application: Application) { private val tasksRemoteDataSource: TasksDataSource private val tasksLocalDataSource: TasksDataSource // Some other code init { val database = Room.databaseBuilder(application.applicationContext, ToDoDatabase::class.java, "Tasks.db") .build() tasksRemoteDataSource = TasksRemoteDataSource tasksLocalDataSource = TasksLocalDataSource(database.taskDao()) } // Rest of class }
下面是使用构造注入方式的代码
class DefaultTasksRepository( private val tasksRemoteDataSource: TasksDataSource, private val tasksLocalDataSource: TasksDataSource, private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) { // Rest of class }
这样我们就实现了解耦,可以在单元测试中进行测试了
为了实现测试,我们需要自己实现一个fakeDataSource,里面对虚拟数据集合进行维护
在测试的时候,我们直接使用fakeDataSource进行
完整代码:
@ExperimentalCoroutinesApi class DefaultTasksRepositoryTest { private val task1 = Task("Title1", "Description1") private val task2 = Task("Title2", "Description2") private val task3 = Task("Title3", "Description3") private val remoteTasks = listOf(task1, task2).sortedBy { it.id } private val localTasks = listOf(task3).sortedBy { it.id } private val newTasks = listOf(task3).sortedBy { it.id } private lateinit var tasksRemoteDataSource: FakeDataSource private lateinit var tasksLocalDataSource: FakeDataSource // Class under test private lateinit var tasksRepository: DefaultTasksRepository @Before fun createRepository() { tasksRemoteDataSource = FakeDataSource(remoteTasks.toMutableList()) tasksLocalDataSource = FakeDataSource(localTasks.toMutableList()) // Get a reference to the class under test tasksRepository = DefaultTasksRepository( tasksRemoteDataSource, tasksLocalDataSource, Dispatchers.Unconfined ) } @Test fun getTasks_requestsAllTasksFromRemoteDataSource() = runBlockingTest{ val tasks = tasksRepository.getTasks(true) as Result.Success assertThat(tasks.data,IsEqual(remoteTasks)) } }
ViewModel层
为什么要测它
作为程序逻辑的主要控制中心,对viewmodel进行测试保证逻辑正确是很有必要的
测它的时候会遇到什么问题
作为View与Model的中间层,ViewModel测试中最大的问题是以下两点
- 双向绑定的LiveData怎么测
- 怎么解决与Model层的依赖问题,如何使用假数据来测试逻辑的正确性
如何解决
- 双向绑定的LiveData怎么测 使用以下工具类利用countdownlatch来将异步过程变为同步过程,从而同步获取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 }
- 怎么解决与Model层的依赖问题,如何使用假数据来测试逻辑的正确性 使用本文测试Model层的方法,构建一个FakeRepository来传入ViewModel的构造方法,需要注意,此时在Fragment或者Activity中构造ViewModel方式有所改变,如下代码
Fragment
class TasksFragment : Fragment() { private val viewModel by viewModels<TasksViewModel> { TasksViewModelFactory((requireContext().applicationContext as TodoApplication).taskRepository) } //... }
ViewModel
class TasksViewModel( private val tasksRepository: TasksRepository ) : ViewModel() { //... } @Suppress("UNCHECKED_CAST") class TasksViewModelFactory ( private val tasksRepository: TasksRepository ) : ViewModelProvider.NewInstanceFactory() { override fun <T : ViewModel> create(modelClass: Class<T>) = (TasksViewModel(tasksRepository) as T) }
完整代码
@RunWith(AndroidJUnit4::class) class TasksViewModelTest{ // Subject under test private lateinit var tasksViewModel: TasksViewModel private lateinit var tasksRepository: FakeTestRepository // Executes each task synchronously using Architecture Components. @get:Rule var instantExecutorRule = InstantTaskExecutorRule() @Before fun setupViewModel() { tasksRepository = FakeTestRepository() val task1 = Task("Title1", "Description1") val task2 = Task("Title2", "Description2", true) val task3 = Task("Title3", "Description3", true) tasksRepository.addTasks(task1, task2, task3) tasksViewModel = TasksViewModel(tasksRepository) } @Test fun addNewTask_setsNewTaskEvent() { // Given a fresh TasksViewModel // When adding a new task tasksViewModel.addNewTask() // Then the new task event is triggered val value =tasksViewModel.newTaskEvent.getOrAwaitValue() assertThat(value.getContentIfNotHandled(),(not(nullValue()))) } @Test fun setFilterAllTasks_tasksAddViewVisible() { // Given a fresh ViewModel // When the filter type is ALL_TASKS tasksViewModel.setFiltering(TasksFilterType.ALL_TASKS) // Then the "Add task" action is visible val value = tasksViewModel.tasksAddViewVisible.getOrAwaitValue() assertThat(value,`is`(true)) } }
为什么不做ui测试
官方给出了一种ui测试的解决方案,但是测试范围仅限于ui是否显示以及ui文字等,完全可以用人工测试替代,而且ui改动后测试用例改动比较频繁,所以ui测试个人觉得没有必要写作单元测试的模式