Skip to content

Commit 2813552

Browse files
authored
Merge pull request #9 from bow-swift/tomas/routing-stacking-responses
Routing and stacking stubbed responses for testing
2 parents b8886a9 + 6b30223 commit 2813552

File tree

7 files changed

+189
-105
lines changed

7 files changed

+189
-105
lines changed

BowOpenAPI.xcodeproj/project.pbxproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@
7070
8BA3B6DD235625F5005D6278 /* libOpenApiGenerator.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B6B3637234CE1F800D3DE25 /* libOpenApiGenerator.a */; };
7171
8BA3B6DF23562730005D6278 /* FileSystemMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BA3B6DE23562730005D6278 /* FileSystemMock.swift */; };
7272
8BC6CF922350DF0400163BD2 /* libOpenApiGenerator.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B6B3637234CE1F800D3DE25 /* libOpenApiGenerator.a */; };
73-
8BD0851D23453DAE006D33DE /* MockRequests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BD0851C23453DAE006D33DE /* MockRequests.swift */; };
73+
8BD0851D23453DAE006D33DE /* Mother.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BD0851C23453DAE006D33DE /* Mother.swift */; };
7474
8BD90F2E23328DAF00281786 /* _param.mustache in CopyFiles */ = {isa = PBXBuildFile; fileRef = 8BDE6909233280FE009342AD /* _param.mustache */; };
7575
8BD90F2F23328DAF00281786 /* AlamofireImplementations.mustache in CopyFiles */ = {isa = PBXBuildFile; fileRef = 8BDE690E233280FE009342AD /* AlamofireImplementations.mustache */; };
7676
8BD90F3023328DAF00281786 /* api.mustache in CopyFiles */ = {isa = PBXBuildFile; fileRef = 8BDE6911233280FE009342AD /* api.mustache */; };
@@ -198,7 +198,7 @@
198198
8BA3B6D4235620B8005D6278 /* ClientGeneratorMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientGeneratorMock.swift; sourceTree = "<group>"; };
199199
8BA3B6DE23562730005D6278 /* FileSystemMock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileSystemMock.swift; sourceTree = "<group>"; };
200200
8BD0851923453A1F006D33DE /* StubURL.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StubURL.swift; sourceTree = "<group>"; };
201-
8BD0851C23453DAE006D33DE /* MockRequests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRequests.swift; sourceTree = "<group>"; };
201+
8BD0851C23453DAE006D33DE /* Mother.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mother.swift; sourceTree = "<group>"; };
202202
8BD085222345E9BD006D33DE /* APIClientSendTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIClientSendTests.swift; sourceTree = "<group>"; };
203203
8BD0866E23476792006D33DE /* APIConfigTesting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIConfigTesting.swift; sourceTree = "<group>"; };
204204
8BD70860233D23280018E06B /* String+Format.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Format.swift"; sourceTree = "<group>"; };
@@ -407,7 +407,7 @@
407407
children = (
408408
8BA3B6D4235620B8005D6278 /* ClientGeneratorMock.swift */,
409409
8BA3B6DE23562730005D6278 /* FileSystemMock.swift */,
410-
8BD0851C23453DAE006D33DE /* MockRequests.swift */,
410+
8BD0851C23453DAE006D33DE /* Mother.swift */,
411411
);
412412
path = Mocks;
413413
sourceTree = "<group>";
@@ -699,7 +699,7 @@
699699
files = (
700700
8B8B1C1723436195009D195A /* API+Generators.swift in Sources */,
701701
8BFE7E41234E10D4003E97BF /* APIClientModelsTests.swift in Sources */,
702-
8BD0851D23453DAE006D33DE /* MockRequests.swift in Sources */,
702+
8BD0851D23453DAE006D33DE /* Mother.swift in Sources */,
703703
8B30B8832348FC1A00E4D852 /* DefaultAPI.swift in Sources */,
704704
8BA3B6D2235613AD005D6278 /* GeneratorAPIClientTests.swift in Sources */,
705705
8B30B87F2348FC1A00E4D852 /* StubURL.swift in Sources */,

Templates/APIConfigTesting.swift

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,45 +7,51 @@ public extension API.Config {
77
/// Stubs data to respond the next HTTP request with.
88
/// - Parameter data: Contents of the response to the next request.
99
/// - Parameter code: HTTP status code for the response. Defaults to 200.
10-
func stub(data: Data, code: Int = 200) -> API.Config {
11-
StubURL.reset()
12-
StubURL.stub(data: data, code: code)
10+
/// - Parameter endpoint: Path to the endpoint which the stubbed data should respond to. Defaults to a generic endpoint that responds to any requests.
11+
func stub(data: Data, code: Int = 200, endpoint: String? = nil) -> API.Config {
12+
StubURL.stub(data: data, code: code, endpoint: endpoint ?? StubURL.anyEndpoint)
1313
return copy(session: URLSession(configuration: URLSessionConfiguration.testing))
1414
}
1515

1616
/// Stubs data to respond the next HTTP request with.
1717
/// - Parameter dataRaw: Contents of the response as String to the next request.
1818
/// - Parameter code: HTTP status code for the response. Defaults to 200.
19-
func stub(dataRaw raw: String, code: Int = 200) -> API.Config {
20-
StubURL.reset()
21-
StubURL.stub(data: raw.data(using: .utf8)!, code: code)
19+
/// - Parameter endpoint: Path to the endpoint which the stubbed data should respond to. Defaults to a generic endpoint that responds to any requests.
20+
func stub(dataRaw raw: String, code: Int = 200, endpoint: String? = nil) -> API.Config {
21+
StubURL.stub(data: raw.data(using: .utf8)!, code: code, endpoint: endpoint ?? StubURL.anyEndpoint)
2222
return copy(session: URLSession(configuration: URLSessionConfiguration.testing))
2323
}
2424

2525
/// Stubs error to respond the next HTTP request with.
2626
/// - Parameter error: error of the response to the next request.
2727
/// - Parameter code: HTTP status code for the response. Defaults to 400.
28-
func stub(error: Error, code: Int = 400) -> API.Config {
29-
StubURL.reset()
30-
StubURL.stub(error: error, code: code)
28+
/// - Parameter endpoint: Path to the endpoint which the stubbed data should respond to. Defaults to a generic endpoint that responds to any requests.
29+
func stub(error: Error, code: Int = 400, endpoint: String? = nil) -> API.Config {
30+
StubURL.stub(error: error, code: code, endpoint: endpoint ?? StubURL.anyEndpoint)
3131
return copy(session: URLSession(configuration: URLSessionConfiguration.testing))
3232
}
3333

3434
/// Stubs a string in JSON format to respond the next HTTP request with.
3535
/// - Parameter json: A string in JSON format with the contents of the response to the next request.
3636
/// - Parameter code: HTTP status code for the response. Defaults to 200.
37-
func stub(json: String, code: Int = 200) -> API.Config {
38-
StubURL.reset()
39-
StubURL.stub(json: json, code: code)
37+
/// - Parameter endpoint: Path to the endpoint which the stubbed data should respond to. Defaults to a generic endpoint that responds to any requests.
38+
func stub(json: String, code: Int = 200, endpoint: String? = nil) -> API.Config {
39+
StubURL.stub(json: json, code: code, endpoint: endpoint ?? StubURL.anyEndpoint)
4040
return copy(session: URLSession(configuration: URLSessionConfiguration.testing))
4141
}
4242

4343
/// Stubs the contents of a file to respond the next HTTP request with.
4444
/// - Parameter url: URL of the file containing the response to the next request.
4545
/// - Parameter code: HTTP status code for the response. Defaults to 200.
46-
func stub(contentsOfFile url: URL, code: Int = 200) -> API.Config {
46+
/// - Parameter endpoint: Path to the endpoint which the stubbed data should respond to. Defaults to a generic endpoint that responds to any requests.
47+
func stub(contentsOfFile url: URL, code: Int = 200, endpoint: String? = nil) -> API.Config {
48+
StubURL.stub(contentsOfFile: url, code: code, endpoint: endpoint ?? StubURL.anyEndpoint)
49+
return copy(session: URLSession(configuration: URLSessionConfiguration.testing))
50+
}
51+
52+
/// Clears all stubs.
53+
func reset() -> API.Config {
4754
StubURL.reset()
48-
StubURL.stub(contentsOfFile: url, code: code)
4955
return copy(session: URLSession(configuration: URLSessionConfiguration.testing))
5056
}
5157
}

Templates/Package.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ let package = Package(
1313
],
1414

1515
targets: [
16-
.target(name: "BowAPIClient", dependencies: ["Bow", "BowEffects", "BowOptics"], path: "Sources"),
17-
.target(name: "BowAPIClientTest", dependencies: ["Bow", "BowEffects", "BowOptics"], path: "XCTest")
16+
.target(name: "BowAPIClient", dependencies: ["Bow", "BowEffects"], path: "Sources"),
17+
.target(name: "BowAPIClientTest", dependencies: ["Bow", "BowEffects"], path: "XCTest")
1818
]
1919
)

Templates/StubURL.swift

Lines changed: 77 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,108 @@
11
// Copyright © 2019 The Bow Authors.
22

33
import Foundation
4+
import Bow
5+
6+
struct StubbedResponse {
7+
let statusCode: Int
8+
let content: Either<Error, Data>
9+
10+
init(data: Data, statusCode: Int = 200) {
11+
self.statusCode = statusCode
12+
self.content = .right(data)
13+
}
14+
15+
init(error: Error, statusCode: Int = 404) {
16+
self.statusCode = statusCode
17+
self.content = .left(error)
18+
}
19+
}
420

521
class StubURL: URLProtocol {
6-
private(set) static var data: Data?
7-
private(set) static var error: Error?
8-
private(set) static var statusCode: Int = 200
22+
static let anyEndpoint = ">>>ANY-ENDPOINT<<<"
23+
private static var stubs: [String: [StubbedResponse]] = [:]
24+
25+
private static func stub(response: StubbedResponse, for endpoint: String) {
26+
if let responses = stubs[endpoint] {
27+
stubs[endpoint] = responses + [response]
28+
} else {
29+
stubs[endpoint] = [response]
30+
}
31+
}
932

10-
/// Stubs methods
11-
class func stub(data: Data, code: Int = 200) {
12-
StubURL.data = data
13-
StubURL.statusCode = code
33+
/// Stub methods
34+
static func stub(data: Data, code: Int = 200, endpoint: String = anyEndpoint) {
35+
stub(response: StubbedResponse(data: data, statusCode: code),
36+
for: endpoint)
1437
}
1538

16-
class func stub(error: Error, code: Int = 400) {
17-
StubURL.error = error
18-
StubURL.statusCode = code
39+
static func stub(error: Error, code: Int = 404, endpoint: String = anyEndpoint) {
40+
stub(response: StubbedResponse(error: error, statusCode: code),
41+
for: endpoint)
1942
}
2043

21-
class func stub(json: String, code: Int = 200) {
22-
stub(data: json.data(using: .utf8)!, code: code)
44+
static func stub(json: String, code: Int = 200, endpoint: String = anyEndpoint) {
45+
stub(response: StubbedResponse(data: json.data(using: .utf8)!),
46+
for: endpoint)
2347
}
2448

25-
class func stub(contentsOfFile url: URL, code: Int = 200) {
49+
static func stub(contentsOfFile url: URL, code: Int = 200, endpoint: String = anyEndpoint) {
2650
let content = try! String(contentsOf: url, encoding: .utf8)
27-
stub(json: content, code: code)
51+
stub(json: content, code: code, endpoint: endpoint)
2852
}
2953

30-
/// Reset the stub to default values: data to nil, error to nil and status code to 200.
31-
class func reset() {
32-
StubURL.data = nil
33-
StubURL.error = nil
34-
StubURL.statusCode = 200
54+
/// Clears all stubs.
55+
static func reset() {
56+
stubs = [:]
57+
}
58+
59+
static func response(for request: URLRequest) -> StubbedResponse? {
60+
if let url = request.url?.absoluteString {
61+
for endpoint in stubs {
62+
if url.hasSuffix(endpoint.key) {
63+
return consume(endpoint.key)
64+
}
65+
}
66+
}
67+
return consume(anyEndpoint)
68+
}
69+
70+
static func consume(_ endpoint: String) -> StubbedResponse? {
71+
let queue = stubs[endpoint]
72+
let response = queue?.first
73+
stubs[endpoint] = Array(queue?.dropFirst() ?? [])
74+
return response
3575
}
3676

3777
// MARK: - URLProtocol methods
38-
override class func canInit(with request:URLRequest) -> Bool {
78+
override static func canInit(with request:URLRequest) -> Bool {
3979
return true
4080
}
4181

42-
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
82+
override static func canonicalRequest(for request: URLRequest) -> URLRequest {
4383
return request
4484
}
4585

4686
override func startLoading() {
87+
guard let stub = StubURL.response(for: request) else {
88+
fatalError("No response stubbed for request: \(request)")
89+
}
90+
4791
let header = request.allHTTPHeaderFields
48-
let response = HTTPURLResponse(url: request.url!, statusCode: StubURL.statusCode, httpVersion: nil, headerFields: header)!
92+
93+
let response = HTTPURLResponse(url: request.url!, statusCode: stub.statusCode, httpVersion: nil, headerFields: header)!
4994
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
5095

51-
if let error = StubURL.error {
52-
client?.urlProtocol(self, didFailWithError: error)
53-
} else {
54-
client?.urlProtocol(self, didLoad: StubURL.data ?? Data())
55-
client?.urlProtocolDidFinishLoading(self)
56-
}
96+
stub.content.fold(
97+
{ error in
98+
client?.urlProtocol(self, didFailWithError: error)
99+
return
100+
},
101+
{ data in
102+
client?.urlProtocol(self, didLoad: data)
103+
client?.urlProtocolDidFinishLoading(self)
104+
return
105+
})
57106
}
58107

59108
override func stopLoading() { }

0 commit comments

Comments
 (0)