Matthew's Dev Blog

SwiftMock - A Mocking Solution for Swift

In September 2015, I tried to write a mocking framework for Swift. It was over-engineered, difficult to use, and quickly abandoned; I decided instead that simple fakes would suffice.

That all changed earlier this year. I was writing much more Swift, both at home (for the Nearly Departed rewrite) and in my day-job, and realised that mocks really are necessary if we want to thoroughly test how our classes interact with collaborators.

tl;dr: SwiftMock is available on GitHub.

Note about my personal test approach

I'm firmly in the "mockist TDD practitioner" camp. I exercise my system-under-test in isolation, and provide test doubles for all of its collaborators. My tests exercise the public interface of the system-under-test; they care that the system-under-test did the right thing, not how it did the right thing.

Various kinds of test doubles

Martin Fowler wrote a great article on mocking and stubbing in 2007, and explains things much better than I ever could. But briefly:

  • a test double is a substitute implementation of a component which your system-under-test collaborates with
  • there are various kinds of test doubles, including:
    • mocks - which have explicit expectations, assert those expectations and will reject unexpected calls
    • stubs - which are primed to simply return data into your system-under-test; they feed data into your system
    • fakes - which are simple reimplementations of a collaborator interface (or Swift protocol)

Let's briefly discuss these.

Mock function calls

I would mock a call if the collaborator is performing a useful action on behalf of the system-under-test. In this case, my test really does care that the expected function is called on the collaborator class, and should fail if that expectation fails.

Examples would be:

  • telling a User object to logout()
  • asking a DataSource object to beginNetworkRequest()
  • asking a Timer object to startTimer(delay: 5)

Mocked function calls:

  • can have any number of parameters which usually must be checked (so the test should fail if you expect startTimer(delay: 5) but the code actually calls startTimer(delay: 10))
  • very often have a Void return type
  • typically have a verb-like name

Stubbed function calls

A stubbed call is simply used to pass data into your system-under-test. In this case, my test doesn't care if this function is called or not; it only cares that the system-under-test produces the correct output.

Examples:

  • getting a UserModel struct from a User object
  • getting the isRequesting state from an object which performs network requests
  • getting the current UserPreferences object from a PreferenceStore collaborator

Stubbed functionality:

  • is either a read-only property, or is a function with no parameters
  • return a non-void result
  • in the real implementation, the function would have no side-effects

This distinction between mocks and stubs is important; while you can use mocks everywhere, this can lead to fragile tests. You'll eventually find yourself adding expectations that you don't care about, just to make the tests pass.

Mixing mocked and stubbed calls in a single test double

Martin's article suggests that a test double would either be a mock, or a stub, or a fake - but I find this choice of mocking or stubbing depends on the function being called, not to the whole object.

Take this protocol as an example of a collaborator:

protocol User {
	var isLoggingIn: Bool { get }
	func login(username: String, password: String)
	func logout()
	func model() -> UserModel
}

isLoggingIn is a read-only property, so is a great candidate for stubbing. Our system-under-test might produce some output like "Logging you in", and our test will assert that this output is generated. But it doesn't care how the system-under-test produced the correct output - because that's testing the internal workings of the system-under-test.

login(username: password:) and logout() functions would be mocked. Their names are verbs, suggesting that they are performing some important action on behalf of our system-under-test.

model() call would be stubbed. It simply returns a value, it's name is a noun, and it could be substituted for a property. My tests doesn't care if this is called or not; but if the system-under-test does call this function, my test would have some other assertions on a different observable outcome.

How SwiftMock can help your project

SwiftMock can help you assert the correctness of your code, if your tests are more in the mockist style - where you substitute all of your collaborators with test doubles.

It will:

  • allow you to set explicit expectations on a function call
  • check the supplied parameters to the call are correct
  • allow you to return values from the call back into the system-under-test
  • optionally perform actions when a function is called (using a doing block), so function arguments can be captured
  • fail-fast if unexpected calls are made

More details

The project README has detailed usage instructions, about how to create and use SwiftMock mocks.

Tagged with:

First published 21 December 2018