Android测试体系-在MVVM架构中如何测试Model层与ViewModel层

4,510 阅读3分钟

背景

此文章是对于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测试中最大的问题是以下两点

  1. 双向绑定的LiveData怎么测
  2. 怎么解决与Model层的依赖问题,如何使用假数据来测试逻辑的正确性

如何解决

  1. 双向绑定的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
}
  1. 怎么解决与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测试个人觉得没有必要写作单元测试的模式