哈Ha 我叫Ilya Smirnov,我是FINCH的一名Android开发人员。 我想向您展示一些我们在团队中开发的使用单元测试的示例。
我们的项目中使用两种类型的单元测试:一致性检查和调用检查。 让我们详细介绍它们中的每一个。
符合性测试
一致性测试检查某些功能执行的实际结果是否与预期结果匹配。 让我向您展示一个示例-假设有一个应用程序显示当天的新闻列表:

有关新闻的数据来自不同的来源,并且在业务层的出口处转换为以下模型:
data class News( val text: String, val date: Long )
根据应用程序逻辑,列表的每个元素都需要以下形式的模型:
data class NewsViewData( val id: String, val title: String, val description: String, val date: String )
以下类将负责将
域模型转换为
视图模型:
class NewsMapper { fun mapToNewsViewData(news: List<News>): List<NewsViewData> { return mutableListOf<NewsViewData>().apply{ news.forEach { val textSplits = it.text.split("\\.".toRegex()) val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale("ru")) add( NewsViewData( id = it.date.toString(), title = textSplits[0], description = textSplits[1].trim(), date = dateFormat.format(it.date) ) ) } } } }
因此,我们知道某个对象
News( "Super News. Some description and bla bla bla", 1551637424401 )
将转换为某些对象
NewsViewData( "1551637424401", "Super News", "Some description and bla bla bla", "2019-03-03 21:23" )
输入和输出数据是已知的,这意味着您可以为
mapToNewsViewData方法编写测试,该方法将根据输入检查输出数据的符合性。
为此,在app / src / test / ...文件夹中,创建
具有以下内容的
NewsMapperTest类:
class NewsMapperTest { private val mapper = NewsMapper() @Test fun mapToNewsViewData() { val inputData = listOf( News("Super News. Some description and bla bla bla", 1551637424401) ) val outputData = mapper.mapToNewsViewData(inputData) Assert.assertEquals(outputData.size, inputData.size) outputData.forEach { Assert.assertEquals(it.id, "1551637424401") Assert.assertEquals(it.title, "Super News") Assert.assertEquals(it.description, "Some description and bla bla bla") Assert.assertEquals(it.date, "2019-03-03 21:23") } } }
使用
org.junit.Assert包中的方法将获得的结果与期望进行比较。 如果任何值不符合预期,则测试将失败。
有时,被测类的构造函数需要一些依赖。 它可以是用于访问资源的简单ResourceManager,也可以是用于执行业务逻辑的完整
Interactor 。 您可以创建此类依赖项的实例,但最好创建类似的模拟对象。 模拟对象提供了一个类的虚拟实现,您可以使用该对象跟踪内部方法的调用并覆盖返回值。
有一个流行的
Mockito框架可以创建模拟。
在Kotlin中,默认情况下所有类都是最终的,因此您不能从头开始在Mockito上创建模拟对象。 要变通解决此限制,建议您添加
mockito-inline依赖项。
如果在编写测试时使用kotlin dsl,则可以使用各种库,例如
Mockito-Kotlin 。
假设
NewsMapper采取某种
NewsRepo依赖项的形式,它记录了有关查看特定新闻
项的用户的信息。 然后,有必要模拟
NewsRepo并根据
isNewsRead的结果检查
mapToNewsViewData方法的返回值。
class NewsMapperTest { private val newsRepo: NewsRepo = mock() private val mapper = NewsMapper(newsRepo) … @Test fun mapToNewsViewData_Read() { whenever(newsRepo.isNewsRead(anyLong())).doReturn(true) ... } @Test fun mapToNewsViewData_UnRead() { whenever(newsRepo.isNewsRead(anyLong())).doReturn(false) ... } … }
因此,模拟对象允许您模拟返回值的各种选项以测试各种测试用例。
除上述示例外,一致性测试还包括各种数据验证器。 例如,一种检查输入的密码中是否存在特殊字符和最小长度的方法。
通话测试
测试调用会检查一个类的方法是否调用另一类的必要方法。 大多数情况下,此类测试应用于
Presenter ,该
Presenter发送View特定命令以更改状态。 返回新闻列表示例:
class MainPresenter( private val view: MainView, private val interactor: NewsInteractor, private val mapper: NewsMapper ) { var scope = CoroutineScope(Dispatchers.Main) fun onCreated() { view.setLoading(true) scope.launch { val news = interactor.getNews() val newsData = mapper.mapToNewsViewData(news) view.setLoading(false) view.setNewsItems(newsData) } } … }
这里最重要的是从
Interactor和
View调用方法的事实。 测试将如下所示:
class MainPresenterTest { private val view: MainView = mock() private val mapper: NewsMapper = mock() private val interactor: NewsInteractor = mock() private val presenter = MainPresenter(view, interactor, mapper).apply { scope = CoroutineScope(Dispatchers.Unconfined) } @Test fun onCreated() = runBlocking { whenever(interactor.getNews()).doReturn(emptyList()) whenever(mapper.mapToNewsViewData(emptyList())).doReturn(emptyList()) presenter.onCreated() verify(view, times(1)).setLoading(true) verify(interactor).getNews() verify(mapper).mapToNewsViewData(emptyList()) verify(view).setLoading(false) verify(view).setNewsItems(emptyList()) } }
可能需要不同的解决方案才能从测试中排除平台依赖性,因为 这一切都取决于使用多线程的技术。 上面的示例使用具有覆盖范围的Kotlin协程来运行测试,如下所示 Dispatchers中使用的主程序代码是指android UI线程,在这种类型的测试中是不可接受的。 使用RxJava将需要其他解决方案,例如,创建一个TestRule来切换代码执行流程。
为了验证方法已被调用,使用了verify方法,该方法可以使用一些方法,这些方法指示对被测试方法的调用次数作为附加参数。
*****
所考虑的测试选项可以覆盖相当大比例的代码,从而使应用程序更加稳定和可预测。 经过测试的代码易于维护,易于扩展,因为 有一定的信心,当添加新功能时,没有任何破坏。 当然,这样的代码更易于重构。
最容易测试的类不包含平台依赖项,因为 使用它时,您不需要第三方解决方案即可创建平台模拟对象。 因此,我们的项目使用的体系结构可最大程度地减少被测层中平台依赖项的使用。
好的代码应该是可测试的。 编写单元测试的复杂性或无能力通常表明被测试的代码出了点问题,是时候考虑重构了。
该示例的源代码可在
GitHub上
找到 。