重构和测试旧版代码

在创建单元测试时,我们所有人都会时不时地挣扎,主要是当MainView依赖于核心/服务类(网络服务,API服务等)时。 有一些设计模式可以帮助我们避免这种情况,但是老实说,并非每次都可行,主要是在处理遗留代码时。 因此,这里有一个易于实施的好“技巧”,可以帮助您进行测试,而无需进行任何重大的重构。

MVVM和面向协议的编程在这里可以帮助我们进行依赖注入。 将这两者结合在一起,将使我们能够创建一个符合您所有ViewModel依赖项的存根类。 我们将其称为ServiceStub

此类将符合我们所有的依赖协议,并将负责在运行测试时为ViewModel提供所需的信息。

听起来很不错,但我们应该开始使用一些代码来了解发生了什么。

  • XCode 9.2以上
  • 斯威夫特4
  • 熟悉Model-View-ViewModel(MVVM)模式
  • 对Nimble或XCTest有所了解
  • 对如何编写单元测试的基本了解
  • 椰子足

代码
要开始使用代码,请转到此处并克隆或下载示例代码,运行pod install并打开工作区。
现在好了,我们准备开始。

该示例代码提供了两个服务类( LocationServiceApiClient )和一个MainView

到目前为止,对服务类的每次调用都在视图控制器上进行。

让我们通过重构视图模型的所有LocationServiceApiClient调用来更改此设置。
我们的第一步将是创建视图模型并重构我们的MainView然后测试视图模型上的所有公共功能。

首先,让我们运行该应用程序。 它使用GitHub API和用户位置来获取iOS开发人员的可用职位,但是用户可以通过键入邮政编码来更改搜索的位置。 每次用户输入新的邮政编码并进行搜索时,地址标签都会更改,并进行呼叫以获取iOS职位。

初始点
让我们创建视图模型。 为此,在“单元测试依赖项教程”内的项目导航器上单击鼠标右键,选择“新建组”并将其命名为“ ViewModels”。

现在在新创建的文件夹中使用command + N ,选择一个Swift File并将其命名为MainViewModel

确保在组区域下方选择两个目标。 (应用和测试目标)

现在,您的文件夹结构应如下所示。

现在,让我们移动func updateCurrentAddress()
MainViewMainViewModel func fetchJobsAround(postalCode: String)方法,并删除与视图控制器相关的所有代码。 添加以下代码:

我们将使用变量addressCompletion将地址传回视图控制器,并将作业结果与函数中的完成块一起addressCompletion 。 有更好的方法将数据传递回视图控制器(反应式方法,通知,委托等),但是出于本教程的考虑,我们的方法可以正常工作。

到现在为止,您的项目应该可以正常构建,以确认command + B 太棒了!

请注意,现在我们使用注入的locationServiceTypeapiClientType变量,而不是LocationService.sharedApiClient.shared

您的Protocols文件应如下所示:

现在,打开LocationService.swift文件并使其符合LocationServiceType协议,并与ApiClient.swift相同,将其打开并使其符合ApiClientType

您可能会觉得我们做的并不多,但是我们正在将服务与我们的视图模型分离,并确保我们的视图模型易于测试。

现在,让我们转到MainView并对其进行更新,以开始使用MainViewModel而不是直接调用services类。

var searchStackView下面的MainView ,添加以下代码:

 私人var viewModel:MainViewModel! 

ViewDidLoad()添加:

  viewModel = MainViewModel(locationServiceType:LocationService.shared,apiClientType:ApiClient.shared,addressCompletion:{ 
self.addressLabel.text =地址
})

在上面的代码中,我们将实例化视图模型,并将LocationService.sharedApiClient.shared作为依赖项进行传递,因为LocationService符合LocationServiceTypeApiClient符合ApiClientType和地址处理程序。

现在是时候重构视图控制器了。
首先,删除服务单例并分别使用viewModel.locationServiceTypeviewModel.apiClientType

现在,让我们处理视图模型完成块以更新标签。 在func updateCurrentAddress()内部添加以下代码。

  viewModel.updateCurrentAddress {[未拥有的自身](地址)在 
self.addressLabel.text =地址
}

调整好视图控制器以使用视图模型后,代码应类似于以下内容:

让我们回顾一下我们到目前为止所做的事情。

第一次更改:
我们创建了一个视图模型,并且视图控制器现在没有与位置服务类的直接交互。 相反,视图控制器调用视图模型以更新用户位置。

原因:
使视图控制器不知道如何提供位置信息。 这样,视图控制器将仅与视图模型进行通信,并且对服务类的任何更改或修改都不需要对视图控制器进行更改。


第二个变化:
我们创建了两个协议LocationServiceTypeApiClientType并分别使LocationServiceApiClient客户端类符合它们。

原因:
只要新的服务等级符合协议,就可以随时更改我们的服务等级而不会影响使用者。


第三个变化:
现在,我们在视图模型上注入位置服务和api客户端依赖项,但不是直接注入,而是注入符合协议的任何对象。

原因:
我们希望视图模型与实际的服务类脱钩。 通过这样做,我们可以创建一个存根服务类ServiceStub ,并使它符合我们的协议以在单元测试中使用它。

在项目导航器上,打开“ unit-test-dependencies-tutorialTests”文件夹。 您应该看到一个子文件夹和两个文件。
其中一个文件是info.plist,我们将不使用它,还有MainViewModelTests
打开MainViewModelTests文件,您应该看到很多样板代码。 让我们从删除所有样板代码开始。

现在,我们需要创建MainViewModel的实例,以能够调用我们要测试的方法。
这是LocationServiceType协议非常有用的地方。
我们的方法是创建一个符合LocationServiceTypeServiceStub类(如果我们对视图模型有更多的依赖关系,他将遵循所有这些依赖关系)。
但是首先,让我们在Stubs文件夹中创建一个名为的新文件。 单击文件夹并command + N ,选择“单元测试用例类”文件,并将其命名为ServiceStub

打开文件并创建ServiceStub类,使其符合LocationServiceTypeApiClientType协议。

必需将protocol.swift文件包含在单元测试目标中。 如果没有,您可以转到文件command + shift + O ,开始键入protocol ,应显示protocol.swift文件,选择它,打开“实用程序”侧边栏(右侧一个),选择“文件”顶部的“检查器”选项和“目标成员身份”上,单击单元测试目标。

应该显示一个错误,单击它可以帮助您添加协议所需的方法。

现在,我们需要添加希望视图模型处理的虚拟数据。 为此,我们需要使用伪数据创建一个MKPlacemark对象。 将以下代码添加到ServiceStub类的currentAddress方法中。 注意,我们导入了MapKitContacts ,我们需要这两个家伙能够创建虚拟数据。 我们需要上述导入来创建伪数据。

首先,让我们创建一个DummyData结构。 里面还有另外两个结构, LocationApi 。 我们将要“伪造”的每个数据中的一个。 我们将使用它在测试中做出断言。 这将有助于我们的测试更加可维护和可扩展。
现在,在ServiceStub类中添加以下代码:

就是这样,现在让我们进行最后的测试运行( command + U )。

最后,我们可以在MainView上进行一些简单的重构和代码组织,以使其更易于理解。

您可能会注意到,我现在正在使用ViewCustomizable协议。 这有助于我们将所有视图控制器的自定义设置在一个地方。 使编辑变得容易,其他开发人员也可以直接加入。

当在视图控制器上使用了多个服务时,此方法特别有用,因为ServiceStub类将符合所有服务协议(您应为每个服务创建一个协议,并且每个协议都应注入视图模型中)并且您可以修改,但是需要ServiceStub类,并且仅使用一个类来处理所有服务。
您不太可能获得像此示例一样简单的旧代码。 但这只是一个示例,说明您一次可以将其中使用了服务/核心类的视图控制器转换为可测试代码的方式。
以我的经验,这对我的一些项目很有帮助,可以将大量的视图控制器转换为可测试的代码。

您可以从此处获取代码。 重构过程的每种状态都有一个分支。