I’ve shown you how to replace real objects with fakes in Swift, so that you can begin creating useful Swift mock objects. We did this by moving away from a concrete type to a protocol. Anything that implements the protocol will work, so fakes are no problem.
But what do we do when we can’t change the signature? How do we substitute fake arguments, or a fake return value?
It’s time to talk about partial mocks in Swift.
Motivation: Stub Returning a Mock
In our TDD sample app, we’re making a Service object that calls the Marvel Comics API. We’ve TDD’d constructing the NSURLSessionDataTask from the request model. And we generate the authentication parameters by injecting a closure.
The next thing is to get the data task to start, by sending it a resume
message. Here are the steps in the Objective-C version of the test:
- Create a mock NSURLSessionDataTask.
- Stub the call to NSURLSession to return this mock data task.
- Exercise the call to the System Under Test.
- Confirm that
resume
was sent to the mock data task.
In Objective-C where mocking is easy, it looks like this using OCMockito:
- (void)test_fetchCharacters_shouldStartDataTask
{
NSURLSessionDataTask *mockDataTask = mock([NSURLSessionDataTask class]);
[given([mockSession dataTaskWithURL:anything() completionHandler:anything()])
willReturn:mockDataTask];
[sut fetchCharactersWithRequestModel:[self dummyRequestModel]];
[verify(mockDataTask) resume];
}
The mockDataTask isn’t actually an NSURLSessionDataTask. But Objective-C doesn’t mind, as long as it responds to the same messages.
Swift Insists on the Correct Type
But Swift won’t let us pass an arbitrary fake object. The return type of the URLSession method is a URLSessionDataTask. If we owned the method, we could change the return type to a protocol. But Swift insists on it being a URLSessionDataTask.
Thankfully, URLSessionDataTask is declared as an open class
. This means we can subclass it. Then we’ll override the method we care about:
final class MockURLSessionDataTask: URLSessionDataTask {
private var resumeCallCount = 0
override func resume() {
resumeCallCount += 1
}
func verifyResume(file: StaticString = #file, line: UInt = #line) {
XCTAssertEqual(resumeCallCount, 1, "call count", file: file, line: line)
}
}
We also need to modify our MockURLSession.
Let’s parameterize the return value by turning it into a property:
var dataTaskReturnValue: URLSessionDataTask!
private var dataTaskCallCount = 0
private var dataTaskLastURL: URL?
func dataTask(with url: URL, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Swift.Void) -> URLSessionDataTask {
dataTaskCallCount += 1
dataTaskLastURL = url
return dataTaskReturnValue;
}
With these tools in place, we can create the test:
func test_fetchCharacters_shouldStartDataTask() {
let mockDataTask = MockURLSessionDataTask()
mockURLSession.dataTaskReturnValue = mockDataTask
sut.fetchCharacters(requestModel: dummyRequestModel())
mockDataTask.verifyResume()
}
The code to pass this test actually blows up the other tests, because it now calls the force-unwrapped dataTaskReturnValue. Let’s fix this by moving the creation and insertion of the mockDataTask into the common test fixture. Swift has led us to do the correct thing here, because we don’t want any actual calls to resume in our unit tests.
Our test is now simply:
func test_fetchCharacters_shouldStartDataTask() {
sut.fetchCharacters(requestModel: dummyRequestModel())
mockDataTask.verifyResume()
}
Avoid That Partial Mock—Except When You Can’t
MockURLSessionDataTask is a “partial mock” because we’re taking a real class and replacing one method.
Under other circumstances, I’d consider this to be a code smell. Here’s the problem: A partial mock mixes test code with production code in a single object.
But Swift gives us no choice. “Subclass and override method” is a classic technique for working around legacy code. It pains me to use this method on new code, but we must have tests.
Here are my rules-of-thumb for mocks in Swift:
- Avoid mocks, except when they’re necessary.
- If you can change the signature, use a protocol-based fake.
- If you can’t change the signature, use a partial mock.
Avoid partial mocks in #SwiftLang—except when you can't
What if you can’t subclass the type to create a partial mock? You may need to call that section of code “non unit testable.” Shed a tear, and retreat to the next available test seam. You may be able to find other ways of automating tests for that section of code. Probably not unit tests, though.
Let’s hope we don’t have to retreat too often!
Do you have any questions, concerns or remarks? Please leave a comment below.
[This post is part of the series TDD Sample App: The Complete Collection …So Far]
I have experienced exactly these limitations a year ago when I was working with CoreBluetooth. I had a class that was dependent of the delegate callbacks of the CBCentralManager. Some of these callback methods passes a CBPeripheral object, which can’t be created since it’s constructor is not exposed (it should only be created by the framework). I was doing the protocol-approach most of the time. However, if you are changing the signature of delegate-callbacks to protocol-types, you don’t conform to the delegate anymore. By that time, what I did was to create thin classes that acted as adapters between the delegate protocol-conformance and the delegate for my own protocol-types. This was really boring and cumbersome work, but it did the job.
I think that in most cases, the partial-mock approach you are describing would solve this. But for CBPeripheral for example, the initializer is marked as unavailable so subclassing wouldn’t solve the creation issue. Hope that there is not a lot of these examples in the frameworks…
Thanks for a good blog!
Albin, you give a good description of what to do when all else fails: thin wrappers. Thank you for sharing!
I have experienced similar issues with some classes in UIKit. At the end I used swizzling and Objective-C runtime to create subclasses (subclass normally and then use ObjC runtime to init it). It is not best and it is better than building too many adapters.