Quality Coding
Shares

How to Design Swift Mock Objects

Shares

Last time, we looked at How Does Swift Support Stubbing and Mocking? Swift’s extension declaration opens the way, letting us attach new protocols to existing classes. That much is well-known in the Swift community.

But then what? How do we create useful mock objects? I’ve seen many examples that are okay… to start with. But in the long run, I’m afraid they’ll fall short for ongoing maintenance.

I wrote OCMockito because I wanted Objective-C mock objects that struck a better balance between “precise” and “forgiving”. I also wanted test code that was easier to read and write. What can we do with Swift mock objects?

Creating the mock object shell

Let’s continue the Swift version of the Marvel Browser TDD sample app. I want the FetchCharactersMarvelService to ask a URLSession for a data task. Here’s where we left the production code:

protocol URLSessionProtocol {}

extension URLSession: URLSessionProtocol {}

struct FetchCharactersMarvelService {

    init(session: URLSessionProtocol) {
        // more to come here
    }

}

In the test code, let’s create the outer shell of a URLSession mock object:

class MockURLSession: URLSessionProtocol {}

That works because the protocol is currently empty. But the protocol needs to declare the slice of URLSession’s interface that we want to use. This actually drives us to apply the Interface Segregation Principle to existing classes!

To get the method we want, control-click on URLSession to reach its interface. We want to request a data task with an URL and a completion handler. Copy and paste the declaration from URLSession to URLSessionProtocol, but delete the open qualifier:

protocol URLSessionProtocol {
    func dataTask(with url: URL, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Swift.Void) -> URLSessionDataTask
}

There’s nothing further to do on the production code side. The protocol declares something that URLSession already provides. But now we get an error on the mock object. It needs to implement the method.

As with dynamically-created Objective-C mocks, a Swift mock object can’t diverge from the real object. Not as long as the extension is empty, which guarantees that the protocol is a subset of the full interface.

Here’s the shell of the mock object:

class MockURLSession: URLSessionProtocol {
    func dataTask(with url: URL, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Swift.Void) -> URLSessionDataTask {
        return URLSessionDataTask()
    }
}

It does nothing with the arguments. (This will change.) It returns a newly-created data task, to satisfy its requirements. (This will change.) It’s a skeleton on which we will build.

Was the method called?

The first thing we want to verify is that calling fetchCharacters on our service will ask URLSession to create a data task. So we’ll have the mock object record whether the method was called. Here’s how most people do this:

class MockURLSession: URLSessionProtocol {
    var dataTaskWasCalled = false

    func dataTask(with url: URL, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Swift.Void) -> URLSessionDataTask {
        dataTaskWasCalled = true
        return URLSessionDataTask()
    }
}

Now a test can create the MockURLSession, inject it to the FetchCharactersMarvelService, call fetchCharacters, and confirm the dataTaskWasCalled is true. It works. But it’s not precise.

It’s probably too forgiving.

We usually want to know more than “Was this method called?” Most of the time, my question is, “Was this method called exactly one time?”

This is (sort of) what OCMockito’s verify(mockObject) statement does. It’s shorthand for verifyCount(mockObject, times(1)).

Let’s get closer to this in our Swift mock object. It’s easy. Change from setting a boolean flag to incrementing a call count:

class MockURLSession: URLSessionProtocol {
    var dataTaskCallCount = 0

    func dataTask(with url: URL, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Swift.Void) -> URLSessionDataTask {
        dataTaskCallCount += 1
        return URLSessionDataTask()
    }
}

Here’s a beginning to my first test:

    func testFetchCharacters_ShouldMakeDataTaskForMarvelEndpoint() {
        let mockURLSession = MockURLSession()
        let sut = FetchCharactersMarvelService(session: mockURLSession)
        let requestModel = FetchCharactersRequestModel(namePrefix: "DUMMY", pageSize: 10, offset: 30)

        sut.fetchCharacters(requestModel: requestModel)

        XCTAssertEqual(mockURLSession.dataTaskCallCount, 1)
    }

This small change brings greater precision to our tests! Just this past week, a unit test with a precise call count revealed that my code was making an unwanted second call.

But the change also brings greater flexibility. If there were a need, we could have a test confirm, “The method should be called at least once,” or “It should be called no more than twice.”

Capturing the argument

The test above isn’t finished. Its job is to confirm that the service call “should make data task for Marvel endpoint”. To do that, we need to capture the URL argument.

Since we only need to confirm one call, let’s just capture the last argument:

class MockURLSession: URLSessionProtocol {
    var dataTaskCallCount = 0
    var dataTaskLastURL: URL?

    func dataTask(with url: URL, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Swift.Void) -> URLSessionDataTask {
        dataTaskCallCount += 1
        dataTaskLastURL = url
        return URLSessionDataTask()
    }
}

Now we can access dataTaskLastURL — that is, the URL argument the last time dataTask was called. The test will use this to check its host property:

    func testFetchCharacters_ShouldMakeDataTaskForMarvelEndpoint() {
        let mockURLSession = MockURLSession()
        let sut = FetchCharactersMarvelService(session: mockURLSession)
        let requestModel = FetchCharactersRequestModel(namePrefix: "DUMMY", pageSize: 10, offset: 30)

        sut.fetchCharacters(requestModel: requestModel)

        XCTAssertEqual(mockURLSession.dataTaskCallCount, 1)
        XCTAssertEqual(mockURLSession.dataTaskLastURL?.host, "gateway.marvel.com")
    }

This is a good test. But can we make it better still?

One assertion per test

An important rule of thumb is that a test should have a single assertion. More accurately, it should “assert one truth”. Our test has two assertions, but they work to verify one truth: “A single data task was requested, with a URL that points to the Marvel API.”

But I’m bothered by the rule of thumb. It leads me to ask, what would it look like to have a helper method verify that one truth?

Most of the time, entire arguments are checked for equality. But that’s not the case for this test. Instead, we want to access a property of the argument. This is more obvious in the Objective-C version, using OCMockito’s hasProperty matcher:

[verify(mockSession) dataTaskWithURL:hasProperty(@"host", @"gateway.marvel.com")
                   completionHandler:anything()];

How can we approach this in Swift? Putting on my OCMockito hat, I realize that in most cases, we verify most arguments using an implicit equalTo matcher. A matcher is:

  • a predicate
  • able to describe itself
  • able to describe mismatches

Let’s go with the most essential piece, which is the predicate. We want to evaluate an optional URL. So the signature of the predicate is (URL?) -> Bool. Let’s pass the predicate to a method in MockURLSession:

    func verifyDataTask(urlMatcher: ((URL?) -> Bool)) {
        XCTAssertEqual(dataTaskCallCount, 1)
        XCTAssertTrue(urlMatcher(dataTaskLastURL))
    }

Our test can use this new verify method, passing a closure that checks the URL’s host property:

mockURLSession.verifyDataTask(urlMatcher: { url in url?.host == "gateway.marvel.com" })

Do you see what’s great about this verify method? Because the test specifies a predicate, verifyDataTask can be used for all sorts of tests!

Improving error reporting

The verify method works well in the success case. But the error reporting needs improvement. For starters, we now have a helper with calls to multiple XCTest assertions. To distinguish between them, let’s add a message to the first assertion:

XCTAssertEqual(dataTaskCallCount, 1, "call count")

That’s better. How about the second assertion?

The first assertion is an XCTAssertEqual. If there’s a discrepancy, it will report both values. So we’ll know what the actual call count is. But the second test is an XCTAssertTrue. If it fails, it won’t tell us why. Let’s get more information by reporting the actual URL:

XCTAssertTrue(urlMatcher(dataTaskLastURL), "Actual URL was (dataTaskLastURL)")

One last thing. When the verification fails, what line number is reported? Unless we take extra steps, the line number will point to the assertions in the helper method. But we want the line number in the test code, not the helper.

I’ve already shared how to fix this in How to Make Specialized Test Assertions in Swift. We’ll add file name and line number arguments to the helper. It then passes those arguments to the underlying assertions:

    func verifyDataTask(urlMatcher: ((URL?) -> Bool), file: StaticString = #file, line: UInt = #line) {
        XCTAssertEqual(dataTaskCallCount, 1, "call count", file: file, line: line)
        XCTAssertTrue(urlMatcher(dataTaskLastURL), "Actual URL was \(dataTaskLastURL)", file: file, line: line)
    }

We now have a very useful mock object!

Not quite OCMockito

Our new verifyDataTask method is really helpful. But the semantics are still different from OCMockito. In our Swift code, we check that there’s exactly one call. And we check that the argument satisfies a given predicate.

OCMockito does things differently. It records all invocations to a method. When it’s time to verify calls, it checks to see how many of those calls satisfy its argument matchers.

In other words, the OCMockito version won’t mind if there are other requests for data tasks. It’s just checking that there’s only one to the Marvel API.

This subtle difference makes the OCMockito version less fragile. It’s more accommodating to future changes in the code.

This gives me a clue for how we might make Swift mock objects even more flexible going forward. …See my try! Swift Tokyo talk for more.

Conclusion

For now, here are my 6 recommendations for creating Swift mock objects:

  1. Don’t use a boolean flag. Capture the call count instead.
  2. When you see multiple assertions, extract a helper.
  3. Don’t have the helper test for equality. Pass argument-matching predicates instead.
  4. Disambiguate failures by giving each underlying assertion a separate message.
  5. If the underlying assertion doesn’t already report the actual value, include it in the message.
  6. Add the file and line arguments, passing them to the underlying assertions.

Questions, comments, concerns? Please share your thoughts below!

[This post is part of the series TDD Sample App: The Complete Collection …So Far]

About the Author Jon Reid

Jon is a coach and consultant on iOS Clean Code (Test Driven Development, unit testing, refactoring, design). He's been practicing TDD since 2001. You can learn more about his background, or see what services he can bring to your organization.

follow me on:

Leave a Comment: