使用Swift将单元测试应用于MVVM

在我的上一篇文章“如何不急于MVVM实现”中,我们学习了Model-View-ViewModel(MVVM)架构,并了解了如何使用它来创建简单的Gallery应用程序。 借助MVVM,我们将业务逻辑和表示逻辑与视图逻辑分离。 关注点分离(SoC)使编写单元测试比以往更加容易。 尽管MVVM的想法很简单,但仍然值得一提的是针对各种用例编写单元测试。 因此,在本文中,我们将更进一步,学习如何为MVVM进行单元测试。

简而言之,我将介绍两种技术:

  • 如何设计模拟来模拟不同的网络状态。
  • 如何使用存根测试用户交互。

让我们从上面提到的上一指南中精心制作的Gallery应用程序开始。

一个简单的图库应用

回想一下,简单的图库应用程序具有以下功能:

  1. 该应用程序从500px API获取流行的照片,并在UITableView中列出照片。
  2. 表格视图中的每个单元格都显示照片的标题,描述和创建日期。

该应用程序中的第一个屏幕称为PhotoListViewController ,此图描述了数据流:

APIService负责网络层的工作,例如设置URL,发送请求等。PhotoListViewModelAPIService询问照片对象,并为PhotoListViewController提供呈现接口。 PhotoListViewController是一个简单的View,根据PhotoListViewModel呈现的数据渲染可见元素。

这是我们将在本文中测试的一些用例:

  1. PhotoListViewModel应该从APIService获取数据。
  2. 如果请求失败,则PhotoListViewModel应该显示一条错误消息。

如果用户按下“待售”照片,则PhotoListViewModel应该允许将其切换到详细信息页面。

MVVM和依赖注入

这是PhotoListViewModel的实现的一部分:

在下面的setUp()tearDown()代码段中,注入的想法会更加清楚。 我们使用APIService对象初始化PhotoListViewModel ,但在测试环境中使用MockAPIService对象:

在下一节中,我们将看到如何设计MockAPIService来模拟所有测试用例的不同情况。

行为测试

我们的第一个用例是:

  1. PhotoListViewModel应该从APIService获取数据。

也就是说,我们要检查PhotoListViewModel是否确实从APIService请求数据。

这是模拟的实现:

为了被注入到PhotoListViewModel中 ,该模拟应符合APIServiceProtocol协议。 因此,我们创建所需的方法fetchPopularPhoto(complete 🙂来遵守协议。 现在,我们要确保PhotoListViewModel是否调用了fetchPopularPhoto(complete 🙂方法来获取数据,因此我们在MockAPIService中创建了一个属性isFetchPopularPhotoCalled 。 还记得apiService属性在测试环境中是MockAPIService对象吗? 如果模拟功能fetchPopularPhoto(complete 🙂PhotoListViewModel调用,则isFetchPopularPhotoCalled将设置为true。

这是我们第一次测试的代码:

这是一个简单的测试用例。 sut (SUT,被测系统)是一个PhotoListViewModel实例。 代码段显示,当PhotoListViewModel提取数据时,我们检查它是否调用了fetchPopularPhoto(complete 🙂方法。 通过使用这种技术,我们可以检查PhotoListViewModel是否为依赖项注入调用指定的方法。 换句话说,我们成功地测试了ViewModel的行为。

成功还是失败?

除了行为测试之外,我们还希望查看PhotoListViewModel是否正确处理网络状态。 通过使用DI技术,我们可以通过更改MockAPIService的响应来模拟成功和失败的网络状态。 在本节中,我们将看到如何更改MockAPIService的响应状态。 让我们检查第二个用例,如下例所示:

  1. 如果请求失败,则PhotoListViewModel应该显示一条错误消息。

根据用例,我们编写以下测试代码:

错误对象表示此测试用例的给定条件:由于权限问题,请求将失败。 在设置了给定条件之后,我们调用sut.initFetch()开始获取数据。 然后是一个窍门:我们通过调用fetchFail(error 🙂函数来请求模拟使请求失败。 最后,我们声明PhotoListViewModel的警报消息以查看其是否正确处理了错误。

那么,如何设计MockAPIService? 让我们回到PhotoListViewModel的实现。 initFetch()

initFetch()触发apiService.fetchPopularPhoto(complete 🙂 ,分配回调闭包,并等待apiService调用回调闭包。 回调关闭是关键! 如果我们要模拟失败的请求,则可以在MockAPIService中使用错误触发该关闭。 这是MockAPIService的实现:

在代码段中,当调用fetchPopularPhoto(complete :)时 ,回调闭包将保存到completeClosure中,以供以后使用。 另一方面,对于PhotoListViewModel ,函数调用已完成,但转义的关闭仍未完成。 在调用fetchSuccess()fetchFail(error 🙂之前,不会触发关闭。 最后,当我们调用fetchSuccess()fetchFail(error :)时PhotoListViewModel接收响应数据并继续完成其工作。

现在, MockAPIService能够模拟任何类型的异步请求。 例如,我们可以断言ViewModel的加载状态:在调用fetchSuccess()之前, isLoading应该为true。 这是一项强大的技术,因为它模仿了异步调用,并且可以立即完成。

让我们准备一些存根

ViewModel接收用户交互并根据交互更改显示。 在MVVM中,用户交互被抽象为一组方法,例如userPressed()userSwipe()等。因此,测试用户交互非常简单:我们调用某个方法并声明ViewModel的相应属性。 让我们检查我们要测试的第三个用例:

  1. 如果用户按下“待售”照片,则PhotoListViewModel应该允许将其切换到详细信息页面。

和以前一样,我们首先编写测试代码:

该测试用例描述了一个按下第一个单元格的用户,我们将查看是否允许进行segue。 问题是,我们还没有拿到任何照片,并且缝合线当前处于空状态。 因此,如果我们在此阶段触发sut.userPressed(at 🙂 ,则会引发超出范围的异常。 我们需要先获取一些数据!

我们需要的数据是Photo对象。 我们希望MockAPIService将照片对象返回到PhotoListViewModel ,就像它在生产环境中所应该的一样。 在apiService将数据返回到PhotoListViewModel之后 ,我们可以触发sut.userPressed(at 🙂并声明isAllowSegue 。 我们为测试创建的照片对象称为“存根”。 我不会深入探讨“存根”的定义,但是如果您有兴趣,请查看我有关Core Data破解测试的文章。

MockAPIService的实现现在变为:

我们创建一个数组completePhotos ,以保存存根。 一旦PhotoListViewModel调用fetchPopularPhoto(complete 🙂 ,这些存根将被返回给PhotoListViewModel 。 然后让我们看一下我们的测试代码:

StubGenerator ()。 stubPhotos()生成几个照片对象。 然后,将照片对象分配给mockAPIService.completePhotos。 请求完成后( 调用了嘲笑APIService.fetchSuccess() ), PhotoListViewModel将通过回调闭包接收那些照片存根。 在这些存根的帮助下,我们可以断言某些操作,例如用户在特定的IndexPath上按下等等。

更多测试…

我们能够使用所学的知识来编写更多的测试用例:

  1. 当网络请求开始时,加载动画应该开始。
  2. 请求完成后,加载动画应停止。
  3. 表格视图应正确呈现。
  4. 当用户按下非出售的照片时,它应该显示错误消息。

所有这些都包含在我的GitHub中:

koromiko /教程
教程– https://koromiko1104.wordpress.com github.com的代码

概括

在本文中,我们使用了MockAPIService来检查:

  • 如果SUT与APIService正确交互。
  • 如果SUT正确处理了错误状态。
  • 如果SUT正确处理了用户交互。

结论

还有很多事情要做。 正如我在上一篇文章中提到的那样,MVVM有其局限性和弊端,例如单一职责的反模式以及缺少构建器和路由器。 在编写测试时,我没有涉及绑定和路由的单元测试。 此外,可以更好地处理SUT的状态(有一篇很好的文章讨论了该状态:iOS体系结构:一种基于状态容器的方法)。

但是,MVVM确实消除了编写测试的障碍:测试表示逻辑更容易。 编写用于用户交互的测试也变得很简单。 最重要的是MVVM简单直观。 这是您了解依赖注入和模块组成的想法的一个很好的起点。 在以下文章中,我将不断重构我们的简单Gallery应用程序,使其更加统一和美观。 所以,请继续关注!

参考

测试视图控制器—第1部分— Clean Swift
Swift中的iOS单元测试—第2部分:可测试的体系结构
如何在Swift中对ViewModel进行单元测试— SwiftyJimmy