Swift中的单元和集成测试技术
单元测试 :是软件测试的级别,其中测试软件的各个单元组件。 目的是验证软件的每个单元是否按设计执行。
集成测试 :是一个软件测试级别,其中各个单元组合在一起并作为一组进行测试 。 此测试级别的目的是暴露集成单元之间交互中的错误。
测试网络请求
让我们看一个示例,该示例显示了使用UrlSession
简单网络请求,如图1所示。 这些是我们在此代码中执行的任务
- 创建一个URL以获取资源
- 使用默认
configuration
创建一个dataTask
对象 - 解码从服务器获取的响应
图2显示了符合Codable
协议的响应模型struct
,否则.decode
将引发error
如上图1所示,我们有三个任务单元。 通过将这两种方法组合在一起,我删除了此代码的可测试性。 我们可以打破这些任务,如图3所示
- 首先,我们将url请求创建逻辑分离到单独的单元,以便可以分别测试
- 其次,我们将解析逻辑分离到一个单元,以便该单元可以独立测试
单元测试请求的创建和解析
在这里,我们仅通过制作示例并放入url,将其传递到方法中,并对其返回值进行断言来测试makeRequest方法。 同样,我们可以通过传递一些模拟JSON并对所解析的结果进行断言来测试响应解析。 为了避免do catch
块,我们使用了XCTest
throws
功能
集成测试网络电话
在进行任何集成测试之前,我们首先使逻辑通用。 正如我们所看到的,我们的网络api类仅特定于一个url,并且我们可以使用通用功能来消耗任何种类的url和响应负载
如您在图5中看到的,我们创建了一个APIRequest
协议
如图6所示,我们创建了使用请求类型和urlSession实例初始化的APIRequestLoader类。 由于apiRequest
应该符合APIRequest
协议,因此我们可以访问makeRequest
和parseResponse
方法。
如图7所示, GitHubRequest
实现了APIRequest
协议的APIRequest
和parseResponse
方法,我们将RequestDataType
为String
并将ResponseDataType
设置为MyGitHub
类型
如图8所示,我们正在使用通用api函数。 如果将其与图1进行比较,将会看到不同之处。
URLSessionDataTask集成测试
如图9所示,我们通过按正常调用方式调用loadAPIRequest
方法来测试它,并等待返回结果,但这两者都要求我们的测试必须通过Internet连接运行
我们需要使加载程序类使用伪造的urlSession
,该伪造的urlSession
返回一些模拟数据,而不是访问服务器,这也可以提高测试执行urlSession
。 所以问题是我们如何制作假的或模拟的服务器
使用URLProtocol模拟
URLSession为应用程序提供了高级API,以用于执行网络请求。 表示正在进行的请求的对象,例如URLSession数据测试。 不过,在幕后,还有另一个较低级别的API
URLProtocol
,它执行打开网络连接,编写请求以及回读响应的基础工作。 URLProtocol旨在进行子类化,从而为URL加载系统提供扩展点。
Foundation为HTTPS等常见协议提供了内置协议子类,但是我们可以在测试中通过提供一个模拟协议来覆盖这些子类,该协议可以让我们对即将出现的请求进行断言并提供模拟响应。
URLProtocol通过URLProtocolClient协议将进度传达回系统。
如图10所示,我们使requestHandler
成为静态属性。 我们在此物业网络中提供的内容将作为响应返回。 当我们调用resume
方法时, startloading
函数将调用,而不是去服务器,它立即返回requestHandler
响应。 当我们在dataTask
上调用cancel
, stoploading
方法将调用。
测试配置
如图11所示,我们执行了许多步骤来配置伪造的urlSession
对象
- 声明了
apiRequestLoader
对象类型,具有隐式拆包可选 - 创建了
GitHubRequest
对象来确定请求和响应类型,并具有创建和解析请求的逻辑 - 使用临时创建的会话配置对象,以避免将缓存,cookie或凭据写入磁盘
- 告诉Url会话在请求网络通话时使用
MockURLProtocol
。 每次URL加载系统收到加载URL的请求时,都会搜索注册的协议处理程序来处理该请求。 每个处理程序通过canInitWithRequest
(_ :)方法告诉系统是否可以处理给定的请求。 - 使用上述配置创建了一个URL会话对象
- 使用模拟会话和github请求创建了
apiRequestLoader
对象
如图12所示,这些是将要执行的步骤
- 创建
mockJSONData
json转换为data
对象的mockJSONData
变量 - 定义静态变量
requestHandler
关闭实现 - 创造
expectations
-
fulfill
这些期望,测试方法将等待,直到满足所有期望或指定的超时时间为1秒。 - 调用
loadAPIRequest
方法,由于加载程序正在使用伪造会话,因此它要求dataTask
从服务器获取数据,然后dataTask
搜索自定义MockProtocol,
以来要调用的协议MockProtocol,
它要求MockProtocol
处理该请求,并且MockProtocol
发送requestHandler
静态属性作为响应。
在图12中,我们使用的UrlSession
技术特定于UrlSession
。 快速模拟提供了部分和完全两种口味。
URLSessionDataTask部分模拟
部分模拟,您正在修改现有类型,使其在测试中仅部分表现出不同
通常,当我们要从服务器获取数据时,我们在UrlSession
上调用dataTask
方法,该方法提供UrlSessionDataTask
对象,而在调用UrlSessionDataTask
对象的resume
,实际上会命中服务器。
如图13所示,我们创建了两个模拟子类。
MockUrlSession
是MockUrlSession
的子类。 我们在新的模拟类上添加了三个额外的属性,这些属性是在创建该模拟类时设置的。 该类还重写dataTask
方法。 在dataTask
重写的方法上,我们正在创建MockURLSessionDataTask
对象,并将其传递给完成闭包
MockURLSessionDataTask
是MockURLSessionDataTask
的子类。我们在新的模拟类上添加了一个额外的属性,该属性在调用MockUrlSession
覆盖的dataTask
方法时设置。 在恢复方法时,将执行关闭方法,该方法会将模拟数据返回到MockURLSessionDataTask
完成块。
如图14所示,这些是将要执行的步骤
- 创建了Github请求对象
- 创建了一个模拟会话
- 创建
jsonData
变量,并将json转换为data
对象,并将其分配给mockSession
的data
属性 - 使用模拟会话和github请求创建了
apiLoader
对象 - 创造
expectation
- 调用
loadAPIRequest
方法,因为加载器正在使用模拟会话,MockUrlSession
覆盖的datatask
方法将执行,并将返回MockURLSessionDataTask
。 当loadAPIRequest
调用resume()
MockURLSessionDataTask
将从覆盖的datatask方法的完成块返回我们在步骤3中创建的数据。
当您看到带有重写方法的模拟子类时。 您正在做部分嘲笑
部分模拟=子类+重写方法
部分模拟的问题
他们在WWDC中说
- SDK中的某些类不是为子类设计的,其行为可能有所不同。
- 我们只是部分修改对象。 这意味着我们对对象内部的工作方式都做出了非常困难的假设。
URLSessionDataTask完成模拟
完整的模拟:您将替换整个实现
在URLSession
情况下,我们将用模拟实现完全替换我们的依赖关系,因为我们需要扩展我们的依赖关系,即URLSession.
在完整的模拟中,我们使用协议来完全替代我们的依赖关系。 众所周知,在我们的方法中,我们只有UrlSession
依赖项,可以从服务器获取数据。 如图15所示,我们创建了NetworkingSession
协议,它将完全取代实现
如图16所示,我们通过使用扩展使UrlSession
符合。 现在,如果要从服务器实际获取数据,则需要在UrlSession
调用loadData
而不是dataTask
方法
如图17所示,我们创建了MockNetworkSession
类,该类将用作模拟依赖项注入到我们的方法中,该方法将返回我们提供的数据,而不是直接到达实际服务器
我们在APIRequestLoader类中做了一些更改
- 首先,我们将
urlSession
属性的类型从UrlSession
为NetworkingSession
协议.
可以将符合NetworkingSession
协议的任何对象分配给此属性。 - 我们使用协议方法加载数据。 如果注入
MockNetworkSession
类,它将返回模拟数据;如果传递URLSession
,它将命中服务器
如图19所示,这些是将要执行的步骤
- 创建了Github请求对象
- 创建了一个符合
NetworkingSession
协议的模拟会话 - 创建
jsonData
变量,并将json转换为data
对象,并将其分配给mockSession
的data
属性 - 创建的
apiLoader
对象的会话符合NetworkingSession
和github请求 - 创造
expectation
- 调用
loadAPIRequest
方法,由于加载程序正在使用模拟会话,因此MockUrlSession
loadData
方法将执行,并将从MockUrlSession
对象的loadData
方法的完成块返回我们在步骤3中创建的数据。
下一个
我们将看到如何使用上述模拟技术对CoreLocation进行单元测试
有用的链接
https://www.swiftbysundell.com/posts/mocking-in-swift
https://developer.apple.com/videos/play/wwdc2018/417/?time=623
如何轻松测试iOS联网代码
普遍认为,单元测试应该是第一位的:快速,隔离,可重复,自验证和及时。 可悲的是…… www.hackingwithswift.com 在Swift中使用NSURLProtocol
在本NSURLProtocol教程中,您将学习如何使用URL加载系统和URL方案来添加自定义… www.raywenderlich.com
https://www.hackingwithswift.com/articles/153/how-to-test-ios-networking-code-the-easy-way