重构和测试旧版代码
在创建单元测试时,我们所有人都会时不时地挣扎,主要是当MainView
依赖于核心/服务类(网络服务,API服务等)时。 有一些设计模式可以帮助我们避免这种情况,但是老实说,并非每次都可行,主要是在处理遗留代码时。 因此,这里有一个易于实施的好“技巧”,可以帮助您进行测试,而无需进行任何重大的重构。
MVVM和面向协议的编程在这里可以帮助我们进行依赖注入。 将这两者结合在一起,将使我们能够创建一个符合您所有ViewModel
依赖项的存根类。 我们将其称为ServiceStub
。
此类将符合我们所有的依赖协议,并将负责在运行测试时为ViewModel
提供所需的信息。
听起来很不错,但我们应该开始使用一些代码来了解发生了什么。
- XCode 9.2以上
- 斯威夫特4
- 熟悉Model-View-ViewModel(MVVM)模式
- 对Nimble或XCTest有所了解
- 对如何编写单元测试的基本了解
- 椰子足
代码
要开始使用代码,请转到此处并克隆或下载示例代码,运行pod install
并打开工作区。
现在好了,我们准备开始。
该示例代码提供了两个服务类( LocationService
和ApiClient
)和一个MainView
。
到目前为止,对服务类的每次调用都在视图控制器上进行。
让我们通过重构视图模型的所有LocationService
和ApiClient
调用来更改此设置。
我们的第一步将是创建视图模型并重构我们的MainView
然后测试视图模型上的所有公共功能。
首先,让我们运行该应用程序。 它使用GitHub API和用户位置来获取iOS开发人员的可用职位,但是用户可以通过键入邮政编码来更改搜索的位置。 每次用户输入新的邮政编码并进行搜索时,地址标签都会更改,并进行呼叫以获取iOS职位。
初始点
让我们创建视图模型。 为此,在“单元测试依赖项教程”内的项目导航器上单击鼠标右键,选择“新建组”并将其命名为“ ViewModels”。
现在在新创建的文件夹中使用command + N
,选择一个Swift File并将其命名为MainViewModel
。
确保在组区域下方选择两个目标。 (应用和测试目标)
现在,您的文件夹结构应如下所示。
现在,让我们移动func updateCurrentAddress()
和
从MainView
到MainViewModel
func fetchJobsAround(postalCode: String)
方法,并删除与视图控制器相关的所有代码。 添加以下代码:
我们将使用变量addressCompletion
将地址传回视图控制器,并将作业结果与函数中的完成块一起addressCompletion
。 有更好的方法将数据传递回视图控制器(反应式方法,通知,委托等),但是出于本教程的考虑,我们的方法可以正常工作。
到现在为止,您的项目应该可以正常构建,以确认command + B
太棒了!
请注意,现在我们使用注入的locationServiceType
和apiClientType
变量,而不是LocationService.shared
和ApiClient.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.shared
和ApiClient.shared
作为依赖项进行传递,因为LocationService
符合LocationServiceType
而ApiClient
符合ApiClientType
和地址处理程序。
现在是时候重构视图控制器了。
首先,删除服务单例并分别使用viewModel.locationServiceType
和viewModel.apiClientType
。
现在,让我们处理视图模型完成块以更新标签。 在func updateCurrentAddress()
内部添加以下代码。
viewModel.updateCurrentAddress {[未拥有的自身](地址)在
self.addressLabel.text =地址
}
调整好视图控制器以使用视图模型后,代码应类似于以下内容:
让我们回顾一下我们到目前为止所做的事情。
第一次更改:
我们创建了一个视图模型,并且视图控制器现在没有与位置服务类的直接交互。 相反,视图控制器调用视图模型以更新用户位置。
原因:
使视图控制器不知道如何提供位置信息。 这样,视图控制器将仅与视图模型进行通信,并且对服务类的任何更改或修改都不需要对视图控制器进行更改。
第二个变化:
我们创建了两个协议LocationServiceType
, ApiClientType
并分别使LocationService
和ApiClient
客户端类符合它们。
原因:
只要新的服务等级符合协议,就可以随时更改我们的服务等级而不会影响使用者。
第三个变化:
现在,我们在视图模型上注入位置服务和api客户端依赖项,但不是直接注入,而是注入符合协议的任何对象。
原因:
我们希望视图模型与实际的服务类脱钩。 通过这样做,我们可以创建一个存根服务类ServiceStub
,并使它符合我们的协议以在单元测试中使用它。
在项目导航器上,打开“ unit-test-dependencies-tutorialTests”文件夹。 您应该看到一个子文件夹和两个文件。
其中一个文件是info.plist,我们将不使用它,还有MainViewModelTests
。
打开MainViewModelTests
文件,您应该看到很多样板代码。 让我们从删除所有样板代码开始。
现在,我们需要创建MainViewModel
的实例,以能够调用我们要测试的方法。
这是LocationServiceType
协议非常有用的地方。
我们的方法是创建一个符合LocationServiceType
的ServiceStub
类(如果我们对视图模型有更多的依赖关系,他将遵循所有这些依赖关系)。
但是首先,让我们在Stubs
文件夹中创建一个名为的新文件。 单击文件夹并command + N
,选择“单元测试用例类”文件,并将其命名为ServiceStub
。
打开文件并创建ServiceStub
类,使其符合LocationServiceType
和ApiClientType
协议。
必需将
protocol.swift
文件包含在单元测试目标中。 如果没有,您可以转到文件command + shift + O
,开始键入protocol
,应显示protocol.swift
文件,选择它,打开“实用程序”侧边栏(右侧一个),选择“文件”顶部的“检查器”选项和“目标成员身份”上,单击单元测试目标。
应该显示一个错误,单击它可以帮助您添加协议所需的方法。
现在,我们需要添加希望视图模型处理的虚拟数据。 为此,我们需要使用伪数据创建一个MKPlacemark
对象。 将以下代码添加到ServiceStub
类的currentAddress
方法中。 注意,我们导入了MapKit
和Contacts
,我们需要这两个家伙能够创建虚拟数据。 我们需要上述导入来创建伪数据。
首先,让我们创建一个DummyData
结构。 里面还有另外两个结构, Location
和Api
。 我们将要“伪造”的每个数据中的一个。 我们将使用它在测试中做出断言。 这将有助于我们的测试更加可维护和可扩展。
现在,在ServiceStub
类中添加以下代码:
就是这样,现在让我们进行最后的测试运行( command + U
)。
最后,我们可以在MainView上进行一些简单的重构和代码组织,以使其更易于理解。
您可能会注意到,我现在正在使用ViewCustomizable
协议。 这有助于我们将所有视图控制器的自定义设置在一个地方。 使编辑变得容易,其他开发人员也可以直接加入。
当在视图控制器上使用了多个服务时,此方法特别有用,因为ServiceStub
类将符合所有服务协议(您应为每个服务创建一个协议,并且每个协议都应注入视图模型中)并且您可以修改,但是需要ServiceStub
类,并且仅使用一个类来处理所有服务。
您不太可能获得像此示例一样简单的旧代码。 但这只是一个示例,说明您一次可以将其中使用了服务/核心类的视图控制器转换为可测试代码的方式。
以我的经验,这对我的一些项目很有帮助,可以将大量的视图控制器转换为可测试的代码。
您可以从此处获取代码。 重构过程的每种状态都有一个分支。