当前位置:首页 > 技术文章 > 正文内容

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

u3blog1年前 (2022-10-08)技术文章442

背景

此文章是对于google code lab中《Introduction to Test Double and Dependence injection》 与 《Testing Basics》的总结,本篇主要讲述如何在mvvm架构的android项目中对Model层以及ViewModel层进行测试

Model层

为什么要测它

model层作为数据获取层,主<span style="display: inline-block; width: 0px; overflow: hidden; line-height: 0;" data-mce-type="bookmark" class="mce_SELRES_start"></span>要与network和数据库打交道,我们需要测试其对数据的获取和更新操作逻辑的正确性

测它的时候会遇到什么问题

如上所述,Model层通常和数据库和网络有较强相关性,我们需要测试的只是其对数据的处理逻辑。

如何解决

改变数据源的获取方式,不要使用内部构造的方式,采用依赖注入方法来进行注入
这是通常写法的Repository代码,里面的dataSource是在内部构建,这就造成了测试的时候难以去除逻辑和数据源的耦合,造成无法进行测试

<pre class="theme:github lang:default decode:true " >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
}</pre>

下面是使用构造注入方式的代码

<pre class="theme:github lang:default decode:true " >class DefaultTasksRepository(
private val tasksRemoteDataSource: TasksDataSource,
private val tasksLocalDataSource: TasksDataSource,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) { // Rest of class }</pre>

这样我们就实现了解耦,可以在单元测试中进行测试了

为了实现测试,我们需要自己实现一个fakeDataSource,里面对虚拟数据集合进行维护

在测试的时候,我们直接使用fakeDataSource进行

完整代码:

<pre class="theme:github lang:default decode:true " >@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))
}
}</pre>

ViewModel层

为什么要测它

作为程序逻辑的主要控制中心,对viewmodel进行测试保证逻辑正确是很有必要的

测它的时候会遇到什么问题

作为View与Model的中间层,ViewModel测试中最大的问题是以下两点

  1. 双向绑定的LiveData怎么测

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

    如何解决

  3. 双向绑定的LiveData怎么测
    使用以下工具类利用countdownlatch来将异步过程变为同步过程,从而同步获取livedata的值

    <pre class="lang:default decode:true " >@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
    }</pre>

  4. 怎么解决与Model层的依赖问题,如何使用假数据来测试逻辑的正确性
    使用本文测试Model层的方法,构建一个FakeRepository来传入ViewModel的构造方法,需要注意,此时在Fragment或者Activity中构造ViewModel方式有所改变,如下代码

Fragment

<pre class="lang:default decode:true " >class TasksFragment : Fragment() {

private val viewModel by viewModels<TasksViewModel> {
TasksViewModelFactory((requireContext().applicationContext as TodoApplication).taskRepository)
}
//...
}</pre>

ViewModel

<pre class="lang:default decode:true " >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)
}
</pre>

完整代码

<pre class="lang:default decode:true " >@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))
}
}</pre>

为什么不做ui测试

官方给出了一种ui测试的解决方案,但是测试范围仅限于ui是否显示以及ui文字等,完全可以用人工测试替代,而且ui改动后测试用例改动比较频繁,所以ui测试个人觉得没有必要写作单元测试的模式

扫描二维码推送至手机访问。

版权声明:本文由u3blog发布,如需转载请注明出处。

本文链接:https://u3blog.xyz/?id=694

分享给朋友:

“Android测试体系-在MVVM架构中如何测试Model层与ViewModel层” 的相关文章

开源数据库Postgresql安装/卸载总结

What Postgresql一个开源数据库,类似mysql,由于mysql被收购了,这个数据库正被越来越多的使用 怎么安装?安装非常简单,但是安装过后的初始化还是有点麻烦,具体可以看这篇文章为什么要卸载?安装好之后,如果你很倒霉的话,会遇到postgresql服务怎么都启动不了,输入psql指令显...

AndroidStudio插件开发——RemoveButterKnife从构思到实现

AndroidStudio插件开发——RemoveButterKnife从构思到实现

ReomveButterKnife插件这是一个用于移除代码中对ButterKnife使用的AS插件,接下来我们将从头开始讲讲AS插件开发和这个插件的开发过程地址是<a href="https://github.com/u3shadow/RemoveButterKnife"...

项目的改造——RemoveButterKnife插件代码的重构

项目的改造——RemoveButterKnife插件代码的重构

前言这篇文章记述了我的插件RemoveButterKnife的代码改进过程以及思路,关于插件,各位可以看RemoveButterKnife代码库,关于文章,可以看构思到实现RemoveButterKnife 原因近期想给原来的插件RemoveButterKnife加入一些新的功能,发现以前的代码没...

项目的升级-给RemoveButterKnife插件增加新功能

项目的升级-给RemoveButterKnife插件增加新功能

前言经过项目的初步编写和进一步改造,RemoveButterKnife插件终于也有模有样了,但是,功能上仅仅支持Activity/Fragment的BindView注解。 关于编写和优化的过程可以看下面两篇文章项目构造RemoveButterKnife 项目改进-重构RemoveButterKn...

使用Databinding为Recyclerview使用同一个ViewHolder加载不同Item

提示:在阅读本篇文章前,你最好对android databinding有一定了解,本文使用的代码均为kotlin,但是不用担心,都很简单 最近在写项目的时候使用了databinding技术,突发奇想,databinding是不是也能应用于recyclerview中,让加载多个不同的item更简单呢...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。