Skip to content

Commit fc74fac

Browse files
authored
Merge pull request #30 from WeTransfer/feature/url-matching-improvements
Improve the way we match registered Mocks
2 parents f2a6754 + 2c08a3b commit fc74fac

File tree

10 files changed

+85
-38
lines changed

10 files changed

+85
-38
lines changed

.travis.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
language: objective-c
2-
osx_image: xcode10.2
2+
osx_image: xcode11.2
33
gemfile: Gemfile
44
bundler_args: "--without documentation --path bundle" # Don't download documentation for gems.
55
cache:

Changelog.md

+3
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
## Changelog
22

33
### Next
4+
5+
### 2.0.0
46
- A new completion callback can be set on `Mock` to use for expectation fulfilling once a `Mock` is completed.
57
- A new onRequest callback can be set on `Mock` to use for expectation fulfilling once a `Mock` is requested.
68
- Updated to Swift 5.0
79
- Only dispatch to the background queue if needed
810
- Correctly handle cancellation of delayed responses
911
- Adding and reading mocks is now thread safe by using a Dispatch Semaphore
1012
- Add support for using Swift Package Manager
13+
- Improved checking for Mocks using `URLRequest`.
1114

1215
### 1.3.0
1316
- Updated to Swift 4.2

Mocker.podspec

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Pod::Spec.new do |spec|
22
spec.name = 'Mocker'
3-
spec.version = '1.3.0'
3+
spec.version = '2.0.0'
44
spec.summary = 'Mock data requests using a custom URLProtocol and run them offline.'
55
spec.description = 'Mocker is a library written in Swift which makes it possible to mock data requests using a custom URLProtocol and run them offline.'
66

@@ -13,7 +13,7 @@ Pod::Spec.new do |spec|
1313
spec.source = { :git => 'https://github.com/WeTransfer/Mocker.git', :tag => spec.version.to_s }
1414
spec.social_media_url = 'https://twitter.com/WeTransfer'
1515

16-
spec.ios.deployment_target = '9.0'
16+
spec.ios.deployment_target = '10.0'
1717
spec.source_files = 'Sources/**/*'
18-
spec.swift_version = '4.2'
18+
spec.swift_version = '5.1'
1919
end

Mocker.xcodeproj/project.pbxproj

+2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
/* Begin PBXFileReference section */
3131
501E26941F3DAE370048F39E /* Mocker.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Mocker.framework; sourceTree = BUILT_PRODUCTS_DIR; };
3232
501E269D1F3DAE370048F39E /* MockerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MockerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
33+
501F8B2D237594AC008EF77E /* Mocker.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = Mocker.podspec; sourceTree = "<group>"; };
3334
503446141F3DB4660039D5E4 /* Mock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Mock.swift; sourceTree = "<group>"; };
3435
503446151F3DB4660039D5E4 /* Mocker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Mocker.swift; sourceTree = "<group>"; };
3536
503446161F3DB4660039D5E4 /* MockingURLProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockingURLProtocol.swift; sourceTree = "<group>"; };
@@ -65,6 +66,7 @@
6566
isa = PBXGroup;
6667
children = (
6768
506277CC235F2777000A4316 /* Changelog.md */,
69+
501F8B2D237594AC008EF77E /* Mocker.podspec */,
6870
501E26951F3DAE370048F39E /* Products */,
6971
50D4606A20653F1F00A85D93 /* Mocker */,
7072
50D4605A20653EAF00A85D93 /* MockerTests */,

MockerTests/MockerTests.swift

+38-10
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ final class MockerTests: XCTestCase {
146146
let mock = Mock(dataType: .json, statusCode: 200, data: [.get: Data()], additionalHeaders: headers)
147147
mock.register()
148148

149-
URLSession.shared.dataTask(with: mock.url) { (_, response, error) in
149+
URLSession.shared.dataTask(with: mock.request) { (_, response, error) in
150150
XCTAssert(error == nil)
151151
XCTAssert(((response as! HTTPURLResponse).allHeaderFields["testkey"] as! String) == "testvalue", "Additional headers should be added.")
152152
expectation.fulfill()
@@ -164,7 +164,7 @@ final class MockerTests: XCTestCase {
164164
let newMock = Mock(dataType: .json, statusCode: 200, data: [.get: Data()], additionalHeaders: ["newkey": "newvalue"])
165165
newMock.register()
166166

167-
URLSession.shared.dataTask(with: mock.url) { (_, response, error) in
167+
URLSession.shared.dataTask(with: mock.request) { (_, response, error) in
168168
XCTAssert(error == nil)
169169
XCTAssert(((response as! HTTPURLResponse).allHeaderFields["newkey"] as! String) == "newvalue", "Additional headers should be added.")
170170
expectation.fulfill()
@@ -199,13 +199,13 @@ final class MockerTests: XCTestCase {
199199
}
200200

201201
/// It should be possible to test cancellation of requests with a delayed mock.
202-
func testDelayedMock() {
202+
func testDelayedMockCancelation() {
203203
let expectation = self.expectation(description: "Data request should be cancelled")
204204
var mock = Mock(dataType: .json, statusCode: 200, data: [.get: Data()])
205205
mock.delay = DispatchTimeInterval.seconds(5)
206206
mock.register()
207207

208-
let task = URLSession.shared.dataTask(with: mock.url) { (_, _, error) in
208+
let task = URLSession.shared.dataTask(with: mock.request) { (_, _, error) in
209209
XCTAssert(error?._code == NSURLErrorCancelled)
210210
expectation.fulfill()
211211
}
@@ -263,18 +263,46 @@ final class MockerTests: XCTestCase {
263263
XCTAssert(mock == urlRequest)
264264
}
265265

266-
/// It should call the completion callback when a `Mock` is used.
267-
func testCompletionCallback() {
268-
let expectation = self.expectation(description: "Data request should succeed")
266+
/// It should call the onRequest and completion callbacks when a `Mock` is used and completed in the right order.
267+
func testMockCallbacks() {
268+
let onRequestExpectation = expectation(description: "Data request should start")
269+
let completionExpectation = expectation(description: "Data request should succeed")
269270
var mock = Mock(dataType: .json, statusCode: 200, data: [.get: Data()])
271+
mock.onRequest = {
272+
onRequestExpectation.fulfill()
273+
}
270274
mock.completion = {
271-
expectation.fulfill()
275+
completionExpectation.fulfill()
272276
}
273277
mock.register()
274278

275-
URLSession.shared.dataTask(with: mock.url).resume()
279+
URLSession.shared.dataTask(with: mock.request).resume()
280+
281+
wait(for: [onRequestExpectation, completionExpectation], timeout: 2.0, enforceOrder: true)
282+
}
283+
284+
/// It should call the mock after a delay.
285+
func testDelayedMock() {
286+
let nonDelayExpectation = expectation(description: "Data request should succeed")
287+
let delayedExpectation = expectation(description: "Data request should succeed")
288+
var delayedMock = Mock(dataType: .json, statusCode: 200, data: [.get: Data()])
289+
delayedMock.delay = DispatchTimeInterval.seconds(1)
290+
delayedMock.completion = {
291+
delayedExpectation.fulfill()
292+
}
293+
delayedMock.register()
294+
var nonDelayMock = Mock(dataType: .json, statusCode: 200, data: [.post: Data()])
295+
nonDelayMock.completion = {
296+
nonDelayExpectation.fulfill()
297+
}
298+
nonDelayMock.register()
299+
300+
XCTAssertNotEqual(delayedMock.request.url!, nonDelayMock.request.url!)
301+
302+
URLSession.shared.dataTask(with: delayedMock.request).resume()
303+
URLSession.shared.dataTask(with: nonDelayMock.request).resume()
276304

277-
waitForExpectations(timeout: 2.0, handler: nil)
305+
wait(for: [nonDelayExpectation, delayedExpectation], timeout: 2.0, enforceOrder: true)
278306
}
279307

280308
/// It should remove all registered mocks correctly.

Sources/Mock.swift

+25-6
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,22 @@ public struct Mock: Equatable {
6464
/// The HTTP status code to return with the response.
6565
public let statusCode: Int
6666

67-
/// The URL value generated based on the Mock data.
68-
public let url: URL
67+
/// The URL value generated based on the Mock data. Force unwrapped on purpose. If you access this URL while it's not set, this is a programming error.
68+
public var url: URL {
69+
if urlToMock == nil && !data.keys.contains(.get) {
70+
assertionFailure("For non GET mocks you should use the `request` property so the HTTP method is set.")
71+
}
72+
return urlToMock ?? generatedURL
73+
}
74+
75+
/// The URL to mock as set implicitely from the init.
76+
private let urlToMock: URL?
77+
78+
/// The URL generated from all the data set on this mock.
79+
private let generatedURL: URL
80+
81+
/// The `URLRequest` to use if you did not set a specific URL.
82+
public let request: URLRequest
6983

7084
/// If `true`, checking the URL will ignore the query and match only for the scheme, host and path.
7185
public let ignoreQuery: Bool
@@ -86,7 +100,12 @@ public struct Mock: Equatable {
86100
public var onRequest: (() -> Void)?
87101

88102
private init(url: URL? = nil, ignoreQuery: Bool = false, dataType: DataType, statusCode: Int, data: [HTTPMethod: Data], additionalHeaders: [String: String] = [:], fileExtensions: [String]? = nil) {
89-
self.url = url ?? URL(string: "https://mocked.wetransfer.com/\(dataType.rawValue)/\(statusCode)/")!
103+
self.urlToMock = url
104+
let generatedURL = URL(string: "https://mocked.wetransfer.com/\(dataType.rawValue)/\(statusCode)/\(data.keys.first!.rawValue)")!
105+
self.generatedURL = generatedURL
106+
var request = URLRequest(url: url ?? generatedURL)
107+
request.httpMethod = data.keys.first!.rawValue
108+
self.request = request
90109
self.ignoreQuery = ignoreQuery
91110
self.dataType = dataType
92111
self.statusCode = statusCode
@@ -158,16 +177,16 @@ public struct Mock: Equatable {
158177
guard let pathExtension = request.url?.pathExtension else { return false }
159178
return fileExtensions.contains(pathExtension)
160179
} else if mock.ignoreQuery {
161-
return mock.url.baseString == request.url?.baseString && mock.data.keys.contains(requestHTTPMethod)
180+
return mock.request.url!.baseString == request.url?.baseString && mock.data.keys.contains(requestHTTPMethod)
162181
}
163182

164-
return mock.url.absoluteString == request.url?.absoluteString && mock.data.keys.contains(requestHTTPMethod)
183+
return mock.request.url!.absoluteString == request.url?.absoluteString && mock.data.keys.contains(requestHTTPMethod)
165184
}
166185

167186
public static func == (lhs: Mock, rhs: Mock) -> Bool {
168187
let lhsHTTPMethods: [String] = lhs.data.keys.compactMap { $0.rawValue }
169188
let rhsHTTPMethods: [String] = lhs.data.keys.compactMap { $0.rawValue }
170-
return lhs.url.absoluteString == rhs.url.absoluteString && lhsHTTPMethods == rhsHTTPMethods
189+
return lhs.request.url!.absoluteString == rhs.request.url!.absoluteString && lhsHTTPMethods == rhsHTTPMethods
171190
}
172191
}
173192

Sources/Mocker.swift

+7-14
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ public struct Mocker {
2929
/// URLs to ignore for mocking.
3030
private(set) var ignoredURLs: [URL] = []
3131

32-
private let mutex = DispatchSemaphore(value: 1)
32+
/// For Thread Safety access.
33+
private let queue = DispatchQueue(label: "mocker.mocks.access.queue", attributes: .concurrent)
3334

3435
private init() {
3536
// Whenever someone is requesting the Mocker, we want the URL protocol to be activated.
@@ -40,7 +41,7 @@ public struct Mocker {
4041
///
4142
/// - Parameter mock: The Mock to be registered for future requests.
4243
public static func register(_ mock: Mock) {
43-
shared.mutex.lock {
44+
shared.queue.async(flags: .barrier) {
4445
/// Delete the Mock if it was already registered.
4546
shared.mocks.removeAll(where: { $0 == mock })
4647
shared.mocks.append(mock)
@@ -51,7 +52,7 @@ public struct Mocker {
5152
///
5253
/// - Parameter url: The URL to mock.
5354
public static func ignore(_ url: URL) {
54-
return shared.mutex.lock {
55+
shared.queue.async(flags: .barrier) {
5556
shared.ignoredURLs.append(url)
5657
}
5758
}
@@ -61,14 +62,14 @@ public struct Mocker {
6162
/// - Parameter url: The URL to check for.
6263
/// - Returns: `true` if it should be mocked, `false` if the URL is registered as ignored.
6364
public static func shouldHandle(_ url: URL) -> Bool {
64-
return shared.mutex.lock {
65+
shared.queue.sync {
6566
return !shared.ignoredURLs.contains(url)
6667
}
6768
}
6869

6970
/// Removes all registered mocks. Use this method in your tearDown function to make sure a Mock is not used in any other test.
7071
public static func removeAll() {
71-
shared.mutex.lock {
72+
shared.queue.sync(flags: .barrier) {
7273
shared.mocks.removeAll()
7374
}
7475
}
@@ -78,7 +79,7 @@ public struct Mocker {
7879
/// - Parameter request: The request to search for a mock.
7980
/// - Returns: A mock if found, `nil` if there's no mocked data registered for the given request.
8081
static func mock(for request: URLRequest) -> Mock? {
81-
return shared.mutex.lock {
82+
shared.queue.sync {
8283
/// First check for specific URLs
8384
if let specificMock = shared.mocks.first(where: { $0 == request && $0.fileExtensions == nil }) {
8485
return specificMock
@@ -88,11 +89,3 @@ public struct Mocker {
8889
}
8990
}
9091
}
91-
92-
private extension DispatchSemaphore {
93-
func lock<T>(execute task: () throws -> T) rethrows -> T {
94-
wait()
95-
defer { signal() }
96-
return try task()
97-
}
98-
}

Sources/MockingURLProtocol.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public final class MockingURLProtocol: URLProtocol {
2121
override public func startLoading() {
2222
guard
2323
let mock = Mocker.mock(for: request),
24-
let response = HTTPURLResponse(url: mock.url, statusCode: mock.statusCode, httpVersion: Mocker.httpVersion.rawValue, headerFields: mock.headers),
24+
let response = HTTPURLResponse(url: mock.request.url!, statusCode: mock.statusCode, httpVersion: Mocker.httpVersion.rawValue, headerFields: mock.headers),
2525
let data = mock.data(for: request)
2626
else {
2727
// swiftlint:disable nslog_prohibited

fastlane/Fastfile

+4-2
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,15 @@ lane :test do |options|
1919
clean: true,
2020
fail_build: false,
2121
code_coverage: true,
22-
formatter: "xcpretty-json-formatter"
22+
formatter: "xcpretty-json-formatter",
23+
result_bundle: true,
24+
output_directory: "build/reports/"
2325
)
2426
rescue => ex
2527
UI.error("Tests failed: #{ex}")
2628
end
2729

28-
trainer(output_directory: "build/reports/", fail_build: false)
30+
trainer(path: "build/reports/", output_directory: "build/reports/", fail_build: false)
2931

3032
validate_changes(project_name: options[:project_name])
3133
end

0 commit comments

Comments
 (0)