原文地址git
在本文中,咱们将讨论测试101的开始:依赖注入。 假设您正在编写测试。github
若是您的测试目标(SUT,系统测试)在某种程度上与现实世界(如联网和CoreData)相关,那么编写测试代码就会更加复杂。基本上,咱们不但愿咱们的测试代码依赖于现实世界的东西。SUT不该依赖于其余复杂系统,以便咱们可以更快地测试它,时不变和环境不变。此外,重要的是咱们的测试代码不会“污染”生产环境。污染是什么意思?这意味着咱们的测试代码向数据库写入一些测试内容,向生产服务器提交一些测试数据,等等。这就是“依赖注入”存在的缘由。数据库
让咱们从一个例子开始。编程
给定一个应该在生产环境中经过internet执行的类。internet部分称为该类的“依赖项”。如上所述,当咱们运行测试时,该类的internet部分必须可以被一个模拟的或假的环境所替代。换句话说,这个类的依赖关系必须是“可注入的”。依赖注入使咱们的系统更加灵活。咱们能够在生产代码中“注入”真实的网络环境。同时,咱们还能够“注入”模拟网络环境以在不访问internet的状况下运行测试代码。swift
本文中,咱们将讨论如下内容:api
咱们将实现一个类“HttpClient”,它应该知足如下要求bash
因此,咱们实现了 HttpClient:服务器
class HttpClient {
typealias completeClosure = ( _ data: Data?, _ error: Error?)->Void
func get( url: URL, callback: @escaping completeClosure ) {
let request = URLRequest(url: url)
request.httpMethod = "GET"
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
callback(data, error)
}
task.resume()
}
}
复制代码
HttpClient 彷佛能够提交一个“GET”请求,并经过闭包“回调”传递返回值。网络
HttpClient().get(url: url) { (success, response) in // Return data }
复制代码
HttpClient 的使用:session
问题是:咱们如何测试它?咱们如何确保代码知足上面列出的需求?直观地说,咱们能够执行代码,将URL分配给HttpClient,而后在控制台中观察结果。然而,这样作意味着咱们每次实现HttpClient时都必须链接到internet。若是测试URL位于生产服务器上,状况彷佛更糟:您的测试运行确实会在必定程度上影响性能,而且您的测试数据将提交给现实世界。如前所述,咱们必须使HttpClient“可测试”。
让咱们看一下URLSession。URLSession是HttpClient的一种“环境”,它是internet的网关。还记得咱们说的“可测试”代码吗?咱们必须使互联网的组成部分是可替换的。因此咱们编辑HttpClient:
class HttpClient {
typealias completeClosure = ( _ data: Data?, _ error: Error?)->Void
private let session: URLSessionProtocol
init(session: URLSessionProtocol) {
self.session = session
}
func get( url: URL, callback: @escaping completeClosure ) {
let request = URLRequest(url: url)
request.httpMethod = "GET"
let task = session.dataTask(with: request) { (data, response, error) in
callback(data, error)
}
task.resume()
}
}
复制代码
We replace the
let task = URLSession.shared.dataTask()
复制代码
with
let task = session.dataTask()
复制代码
而后咱们添加一个新的变量:session,添加一个相应的init。从如今开始,当咱们建立HttpClient时,咱们必须分配session。也就是说,咱们必须将会话“注入”到咱们建立的任何HttpClient对象。如今咱们可使用' URLSession运行生产代码了。并使用一个被注入的模拟会话运行测试代码。
HttpClient的使用变成了:
HttpClient(session: SomeURLSession()).get(url: url){(data,response, error) in
// Return data
}
复制代码
为这个HttpClient编写测试代码变得很是容易。因此咱们设置测试环境:
class HttpClientTests: XCTestCase {
var httpClient: HttpClient!
let session = MockURLSession()
override func setUp() {
super.setUp()
httpClient = HttpClient(session: session)
}
override func tearDown() {
super.tearDown()
}
}
复制代码
这是一个典型的XCTestCase设置。变量httpClient是被测试系统(SUT),变量session是咱们将注入到httpClient的环境。由于咱们在测试环境中运行代码,因此咱们将MockURLSession对象分配给session。而后咱们将模拟会话注入到httpClient。它使httpClient运行在MockURLSession上,而不是URLSession.shared上。
如今咱们关注咱们的第一个要求:
下面是咱们的测试用例:
func test_get_request_withURL() {
guard let url = URL(string: "https://mockurl") else {
fatalError("URL can't be empty")
}
httpClient.get(url: url) { (success, response) in
// Return data
}
// Assert
}
复制代码
这个测试用例能够表示为:
接下来咱们须要编写断言部分。
那么咱们如何知道HttpClient的“get”方法是否提交正确的url呢?让咱们看一下依赖项:URLSession。一般,“get”方法使用给定的url建立一个请求,并将该请求分配给URLSession来提交该请求:
let task = session.dataTask(with: request) { (data, response, error) in
callback(data, error)
}
task.resume()
复制代码
如今,在测试环境中,请求被分配给MockURLSession。所以,咱们能够侵入咱们拥有的MockURLSession,以检查请求是否被正确建立。
这是MockURLSession的实现:
class MockURLSession {
var responseData: Data?
var responseHeader: HTTPURLResponse?
var responseError: Error?
//var sessionDataTask = MockURLSessionDataTask() 待会实现
private (set) var lastURL: URL?
func dataTask(with request: URLRequest, completionHandler: @escaping DataTaskResult) -> URLSessionDataTask {
lastURL = request.url
completionHandler(responseData, responseHeader, responseError)
return // dataTask, will be impletmented later
}
}
复制代码
MockURLSession的做用相似于URLSession。URLSession和MockURLSession都有相同的方法dataTask()和相同的回调闭包类型。尽管URLSession中的dataTask()执行的任务比MockURLSession多,但它们的接口看起来很类似。因为使用相同的接口,咱们可以用MockURLSession替代URLSession,而不须要更改太多“get”方法的代码。而后咱们建立一个变量lastURL,以跟踪在“get”方法中提交的最终url。简单地说,在测试时,咱们建立一个HttpClient,将MockURLSession注入其中,而后查看先后的url是否相同。
测试用例将长这样:
func test_get_request_withURL() {
guard let url = URL(string: "https://mockurl") else {
fatalError("URL can't be empty")
}
httpClient.get(url: url) { (success, response) in
// Return data
}
XCTAssert(session.lastURL == url)
}
复制代码
咱们断言带有url的lastURL,以查看“get”方法是否正确地用正确的url建立请求。
在上面的代码中,还有一件事须要实现:return // dataTask。在URLSession中,返回值必须是URLSessionDataTask。然而,URLSessionDataTask不能经过编程方式建立,所以,这是一个须要模拟的对象:
class MockURLSessionDataTask {
func resume() { }
}
复制代码
与URLSessionDataTask同样,这个mock具备相同的方法resume()。所以,它能够将这个mock处理为dataTask()的返回值。
而后,你会发现一些编译错误在你的代码:
class HttpClientTests: XCTestCase {
var httpClient: HttpClient!
let session = MockURLSession()
override func setUp() {
super.setUp()
httpClient = HttpClient(session: session) // Doesn't compile } override func tearDown() { super.tearDown() } } 复制代码
MockURLSession的接口与URLSession的接口不一样。所以,当咱们尝试注入MockURLSession时,编译器将没法识别它。咱们必须使模拟对象的接口与真实对象相同。那么,让咱们来介绍一下“协议”!
HttpClient的依赖关系是: private let session: URLSession
咱们但愿会话是URLSession或MockURLSession。因此咱们把类型从URLSession改成协议URLSessionProtocol: private let session: URLSessionProtocol
如今咱们能够注入URLSession或MockURLSession或任何符合此协议的对象。
这是协议的实现:
protocol URLSessionProtocol { typealias DataTaskResult = (Data?, URLResponse?, Error?) -> Void
func dataTask(with request: NSURLRequest, completionHandler: @escaping DataTaskResult) -> URLSessionDataTaskProtocol
}
复制代码
在咱们的测试代码中,咱们只须要一个方法:dataTask(NSURLRequest, DataTaskResult)
,所以咱们在协议中只定义了一个必需的方法。当咱们想要嘲笑咱们并不拥有的东西时,一般会采用这种技巧。
还记得MockURLDataTask吗?这是另外一个咱们不拥有的东西,咱们会建立另外一个协议。
protocol URLSessionDataTaskProtocol { func resume() }
复制代码
We also have to make the real objects conform the protocols.
extension URLSession: URLSessionProtocol {}
extension URLSessionDataTask: URLSessionDataTaskProtocol {}
复制代码
URLSessionDataTask具备彻底相同的协议方法resume(),所以URLSessionDataTask不会发生任何事情。
问题是,URLSession没有dataTask()返回URLSessionDataTaskProtocol。所以,咱们须要扩展一个方法来遵照协议。
extension URLSession: URLSessionProtocol {
func dataTask(with request: NSURLRequest, completionHandler: @escaping DataTaskResult) -> URLSessionDataTaskProtocol {
return dataTask(with: request, completionHandler: completionHandler) as URLSessionDataTask as URLSessionDataTaskProtocol
}
}
复制代码
这是一个将返回类型从URLSessionDataTask转换为URLSessionDataTaskProtocol的简单方法。它根本不会改变dataTask()的行为。
如今咱们能够完成MockURLSession中缺失的部分了:
class MockURLSession: URLSessionProtocol {
var responseData: Data?
var responseHeader: HTTPURLResponse?
var responseError: Error?
var sessionDataTask = MockURLSessionDataTask()
private(set) var lastURL: URL?
/// 返回值 URLSessionDataTask 不能经过编程方式建立,所以,这是一个须要模拟的对象:
func dataTask(with request: URLRequest, completionHandler: @escaping DataTaskHandler) -> URLSessionDataTaskProtocol {
lastURL = request.url
completionHandler(responseData, responseHeader, responseError)
return sessionDataTask
}
}
}
复制代码
We know the // dataTask… could be a MockURLSessionDataTask:
class MockURLSession: URLSessionProtocol {
var nextDataTask = MockURLSessionDataTask()
private (set) var lastURL: URL?
func dataTask(with request: NSURLRequest, completionHandler: @escaping DataTaskResult) -> URLSessionDataTaskProtocol {
lastURL = request.url
completionHandler(nextData, successHttpURLResponse(request: request), nextError)
return nextDataTask
}
}
复制代码
这是一个模拟,在咱们的测试环境中相似于URLSession,能够保存url以进行断言
第二个要求是:
咱们但愿确保HttpClient中的“get”方法如期提交请求。 与前一个测试测试数据的正确性不一样,此测试断言是否调用了方法。换句话说,咱们想知道是否调用了URLSessionDataTask.resume()。让咱们玩老把戏:
咱们建立一个新变量 resumewascall 来记录resume()
方法是否被调用。
func test_get_resume_called() {
let dataTask = MockURLSessionDataTask()
session.nextDataTask = dataTask
guard let url = URL(string: "https://mockurl") else {
fatalError("URL can't be empty")
}
httpClient.get(url: url) { (success, response) in
// Return data
}
XCTAssert(dataTask.resumeWasCalled)
}
复制代码
变量dataTask是一个mock,它属于咱们本身,因此咱们能够添加一个属性来测试resume()的行为:
class MockURLSessionDataTask: URLSessionDataTaskProtocol {
private (set) var resumeWasCalled = false
func resume() {
resumeWasCalled = true
}
}
复制代码
若是resume()被调用,那么resumeWasCalled就会变成“true”)很简单,对吧?
在本文中,咱们了解到: 如何适应依赖注入以改变生产/测试环境。 如何利用协议建立模拟。 如何测试传递值的正确性。 如何断言某个函数的行为。 在开始时,您必须花费大量时间编写一个简单的测试。并且,测试代码也是代码,因此您仍然须要使其清晰且结构良好。可是编写测试的好处是无价的。只有经过适当的测试才能扩展代码,而测试能够帮助您避免微小的错误。因此,让咱们开始吧! 示例代码在GitHub上。这是一个游乐场,我在那里增长了一个测试。请随意下载/派生它,并欢迎任何反馈!