Swift中的HTTP模拟测试示例详解
<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li>正文</li><ul class="second_class_ul"><li>StarWarsAPI 类</li><li>MockLoader</li></ul></ul></div><p class="maodian"></p><h2>正文</h2><div class="cros igoods"><div class="goodsin" data-img="https://img14.360buyimg.com/pop/jfs/t1840/116/1507339532/217453/29fbde1b/5667fa5cN164bc29e.jpg" data-name="Swift程序设计实战入门" data-owner="京东自营" data-price="41.2" data-tgid="38" data-url="https://union-click.jd.com/jdc?e=&p=JF8BAMkJK1olXwUFU1xcAUoRA18IG1IWVQMAUW4ZVxNJXF9RXh5UHw0cSgYYXBcIWDoXSQVJQwYCXV1VDUkSHDZNRwYlR3NqKiMFVgB1WBkPEi0TBWRVJlYYeEcbM2gNHF4dXwMBZF5eDkwXAmoIK2sVXDZQOobrvpOysnPcsdTA1ZEyVW5dD00TBGgPG1oVWgMCZF5VDHtUVypcWBhdbTYyV25tOEsnAF9WdVpGWgMKVVdVZhZCQTlbUx1TMwQHVFpaAU8QM20JGlkXbTY"></div></div>
<p>我们已经了解了单个方法如何为通过网络加载请求提供基础。</p>
<p>然而,网络也是开发应用程序时最大的失败点之一,尤其是在单元测试方面。 当我们编写单元测试时,我们希望测试是可重复的:无论我们执行多少次,我们应该总是得到相同的结果。</p>
<p>如果我们的测试涉及实时网络连接,我们无法保证这一点。 由于我们实际网络请求失败的所有原因,我们的单元测试也可能失败。</p>
<p>因此,我们使用模拟对象来模拟网络连接,但实际上提供了一个一致且可重复的外观,我们可以通过它提供虚假数据。</p>
<p>由于我们已将网络接口抽象为单个方法,因此模拟它非常简单。</p>
<p>这是一个始终返回 <code>200 OK</code> 响应的 <code>HTTPLoading</code> 实现:</p>
<div class="jb51code"><pre class="brush:java;">public class MockLoader: HTTPLoading {
public func load(request: HTTPRequest, completion: @escaping (HTTPResult) -> Void) {
let urlResponse = HTTPURLResponse(url: request.url!, statusCode: HTTPStatus(rawValue: 200), httpVersion: "1.1", headerFields: nil)!
let response = HTTPResponse(request: request, response: urlResponse, body: nil)
completion(.success(response))
}
}
</pre></div>
<p>我们可以在任何需要 <code>HTTPLoading</code> 值的地方提供 <code>MockLoader</code> 的实例,发送给它的任何请求都将导致 <code>200 OK</code> 响应,尽管主体为 <code>nil</code>。</p>
<p>当我们使用模拟网络连接编写单元测试时,我们并不是在测试网络代码本身。 通过模拟网络层,我们将网络作为变量移除,这意味着网络不是被测试的对象:单元测试检查实验的变量。</p>
<p class="maodian"></p><h3>StarWarsAPI 类</h3>
<p>我们将使用我们在上一篇文章中删除的 <code>StarWarsAPI</code> 类来说明这一原则:</p>
<div class="jb51code"><pre class="brush:java;">public class StarWarsAPI {
private let loader: HTTPLoading
public init(loader: HTTPLoading = URLSession.shared) {
self.loader = loader
}
public func requestPeople(completion: @escaping (...) -> Void) {
var r = HTTPRequest()
r.host = "swapi.dev"
r.path = "/api/people"
loader.load(request: r) { result in
// TODO: interpret the result
completion(...)
}
}
}
</pre></div>
<p>该类的测试将验证其行为:我们要确保它在不同情况下的行为正确。 例如,我们要确保 <code>requestPeople()</code> 方法在收到 <code>200 OK</code> 响应或 <code>404 Not Found</code> 响应或 <code>500 Internal Server Error</code> 时行为正确。 我们使用 <code>MockLoader</code> 模拟这些场景。 这些测试将使我们有信心在不破坏现有功能的情况下改进 <code>StarWarsAPI</code> 的实现。</p>
<p class="maodian"></p><h3>MockLoader</h3>
<p>为了满足这些需求,我们的 <code>MockLoader</code> 需要:</p>
<p>保证传入的请求是我们在测试中期望的请求 为每个请求提供自定义响应 我个人版本的 <code>MockLoader</code> 大致如下所示:</p>
<div class="jb51code"><pre class="brush:java;">public class MockLoader: HTTPLoading {
// typealiases help make method signatures simpler
public typealias HTTPHandler = (HTTPResult) -> Void
public typealias MockHandler = (HTTPRequest, HTTPHandler) -> Void
private var nextHandlers = Array<MockHandler>()
public override func load(request: HTTPRequest, completion: @escaping HTTPHandler) {
if nextHandlers.isEmpty == false {
let next = nextHandlers.removeFirst()
next(request, completion)
} else {
let error = HTTPError(code: .cannotConnect, request: request)
completion(.failure(error))
}
}
@discardableResult
public func then(_ handler: @escaping MockHandler) -> Mock {
nextHandlers.append(handler)
return self
}
}
</pre></div>
<p>这个 <code>MockLoader</code> 允许我提供如何响应连续请求的个性化实现。 例如:</p>
<div class="jb51code"><pre class="brush:java;">func test_sequentialExecutions() {
let mock = MockLoader()
for i in 0 ..< 5 {
mock.then { request, handler in
XCTAssert(request.path, "/(i)")
handler(.success(...))
}
}
for i in 0 ..< 5 {
var r = HTTPRequest()
r.path = "/(i)"
mock.load(r) { result in
XCTAssertEqual(result.response?.statusCode, .ok)
}
}
}
</pre></div>
<p>如果我们在为 <code>StarWarsAPI</code> 类编写测试时使用这个 <code>MockLoader</code>,它可能看起来像这样(我省略了 <code>XCTestExpectations</code>,因为它们与本次讨论没有直接关系):</p>
<div class="jb51code"><pre class="brush:java;">class StarWarsAPITests: XCTestCase {
let mock = MockLoader()
lazy var api: StarWarsAPI = { StarWarsAPI(loader: mock) }()
func test_200_OK_WithValidBody() {
mock.then { request, handler in
XCTAssertEqual(request.path, "/api/people")
handler(.success(/* 200 OK with some valid JSON */))
}
api.requestPeople { ...
// assert that "StarWarsAPI" correctly decoded the response
}
}
func test_200_OK_WithInvalidBody() {
mock.then { request, handler in
XCTAssertEqual(request.path, "/api/people")
handler(.success(/* 200 OK but some mangled JSON */))
}
api.requestPeople { ...
// assert that "StarWarsAPI" correctly realized the response was bad JSON
}
}
func test_404() {
mock.then { request, handler in
XCTAssertEqual(request.path, "/api/people")
handler(.success(/* 404 Not Found */))
}
api.requestPeople { ...
// assert that "StarWarsAPI" correctly produced an error
}
}
func test_DroppedConnection() {
mock.then { request, handler in
XCTAssertEqual(request.path, "/api/people")
handler(.failure(/* HTTPError of some kind */))
}
api.requestPeople { ...
// assert that "StarWarsAPI" correctly produced an error
}
}
...
}
</pre></div>
<p>当我们编写这样的测试时,我们将 <code>StarWarsAPI</code> 视为一个“黑匣子”:给定特定的输入条件,它是否总是产生预期的输出结果?</p>
<p>我们的 <code>HTTPLoading</code> 抽象使得交换网络堆栈的实现成为一个简单的改变。 我们所做的只是将 <code>MockLoader</code> 传递给初始化程序而不是 <code>URLSession</code>。 这里的关键是意识到,通过使我们的 StarWarsAPI 依赖于接口 (HTTPLoading) 而不是具体化 (URLSession),我们极大地增强了它的实用性并使其更易于单独使用(和测试)。</p>
<p>这种对特定实现的行为定义的依赖将在我们实现框架的其余部分时很好地为我们服务。 在下一篇文章中,我们会将 <code>HTTPLoading</code> 更改为一个类并添加一个属性,该属性将为我们可以想象的几乎所有可能的网络行为提供基础。</p>
<p>以上就是Swift中的HTTP模拟测试示例详解的详细内容,更多关于Swift HTTP模拟测试的资料请关注琼殿技术社区其它相关文章!</p>
<div class="art_xg">
<b>您可能感兴趣的文章:</b><ul><li>Swift中Optional值的链式调用学习笔记</li><li>Swift 中如何使用 Option Pattern 改善可选项的 API 设计</li><li>Swift Package 技巧及混编兼容问题详解</li><li>Swift重构自定义空等运算符 “??=” 实例</li><li>Swift 重构重载运算符示例解析</li><li>Swift HTTP加载请求Loading Requests教程</li><li>Swift中的HTTP请求体Request Bodies使用示例详解</li><li>Swift中的可选项Optional解包方式实现原理</li></ul>
</div>
</div>
<!--endmain-->
頁:
[1]