Unit Testing Masterclass: Mocking like a Pro
Write robust and maintainable software using modern language features
Welcome to Part 2 of my blog series about how to write tests when utilising modern language features like async/await or frameworks like Combine.
Part II Mocking like a Pro
Part III Unit Testing with async/await
Part V (interlude) — Implement your Data Access layer with Combine
Part II - Mocking like a Pro
I appreciate the fact that I’m writing a 6-part blog series about how to write tests, and with 2 posts down I still haven’t shown you a single test - but trust me that I’m giving you the tools to make the actual testing portion an absolute breeze.
What are mocks?
Mocks are versions of your classes and services which conform to the same protocol (interface) as the ‘real’ versions. These are dependency-injected into the classes which you’re testing and allow you fine-tune behaviour. You can do things like:
Count the number of times a method is called
Pass it some sample data to return for a success-case test
Tell it which error to throw to test your exception handling
We use mocks extensively when testing, to ensure that we can test the behaviour of the thing that we’re testing - in unit tests, we don’t care about the behaviour of the thing that we’re injecting.
Quick note on the Test Pyramid
The ‘Test Pyramid’ is an approach to testing:
Unit tests at the bottom of the pyramid - these usually only test one class, have lots of mocks, and we write a lot of them
Integration tests in the middle of the pyramid - these test the interaction between classes, and we should write a decent number for really critical components like our authentication or core business logic.
End-to-end testing at the top of the pyramid - these are a lot slower, test whole features at once, and we write a smaller number of these.
The mock-related thing to note here is that unit tests pretty much exclusively use mocks, whereas in integration tests you might use the ‘real’ version of a service that you inject, to test the real behaviour and interaction between the components.
How do we create mocks?
Creating mocks is pretty straightforward once you have an interface built.
Let’s return to our sample code for Bev. We’re trying to mock out the API, which in Part I we dependency-injected into the Repository class.
Remember, by mocking out the API we should be able to replicate the behaviour of the real API, but control all the results. Here’s the protocol (interface if you’re a fan of Kotlin) for the API:
To make a mock, let’s just define a class, MockBeerAPI, and you’ll see an error in Xcode:
Simply allow Xcode to auto-fill the method required to conform to the protocol, and hey-presto! You’ve got yourself a mock.
In true Art Attack style, here’s one I made earlier:
There’s quite a bit of extra power that I added to the mock - let’s go through it sequentially:
I added a
public init
so that any test class can easily create their own version of this mock.There’s a public
stubBeersResponse
that returns aResult
type - this means I can tell my mock to return either an error or some test data when I set this property.The didGetBeers
closure isdeferred
, which means it gets called after I return from mygetBeers()
method. This is extremely helpful when dealing with async tests, but fairly advanced stuff which I'll cover in future posts.getBeersCallCount
is a good old-fashioned integer property, that simply gets incremented every time thegetBeers
method is called. This is great for writing the common or garden “did this method call this API” test.Finally, we modify
getBeers()
to ensure we call the deferred closure, we increment the call count, and by usingget()
we either return the value set in thestubBeersResponse
Result, throws an error, or crashes if this isn’t set. In the sample code, I’ve actually written helper method called evaluate() which causes an XCTest failure instead of a crash if you forget to set this, but I’m leaving it out of this example for simplicity’s sake.
This is a lot of info to get at once, but this pretty much covers all the things you want a mock to be doing. Most mocks have many functions for the interface they cover, but each function will more or less follow this exact format.
Quick aside on modules
If you delve into the Sample code, you’ll notice that the
MockBeerAPI
- and actually all the mock classes - are defined in their own modules, such as NetworkingMocks or RepositoryMocks. This is to allow separate modules in layers above to import this code, without mixing it all in with our production code - of course, we just want our tests to be using the mocks and don’t want it built as part of the app we send to our users!If your app is single-module, it’s a lot more straightforward - you can just define the mocks as part of your app’s unit test target (and don't need to mark everything
public
!).
Advanced mocking
Now you know how to create a basic mock, you have 90% of the tools you need to mock out any function on any interface you might want to test.
But there are a couple of extra cases you might want to be able to test: Mocking out existing system tools, and mocking out slightly more complex interfaces.
Mocking out system tools
As we saw at the end of the previous post on Dependency Injection, the lowest layer contained our API and networking logic, which had the URLSession
dependency injected into it.
We didn’t write this ourselves - Apple provides this out-of-the-box with Foundation’s URLSession
class. So it’s a little trickier to mock this out, but still doable.
There are a few approaches, but my favoured is to create a new protocol, which copies the function signature of the functions in URLSession
we’re using.
Once we’ve done this, we can conform the existing URLSession
to our new protocol.
This was the tricky part! Now we can simply create another mock in our tests which confirms to this protocol:
It’s structured very similarly to our first mock:
public init
so anything outside the module can create its own mock.stubDataResponse
that allows us to set a Result type with test values or an error.capturedURL
is another classic test property - whenever the API is called with a URL, we set this property so we can test the correct URL is called. You’ll also notice this is property ispublic private(set)
because only this file is meant to modify the property, but any of our test modules should be able to read it.For more details about
public private(set)
, learn how to fine-tune your access control by checking out my post: Access Control Like A Boss.Finally, we implement
data(from url: URL)
as normal to conform to the protocol, capture the URL, and return theResult
which we set up.
Mocking out more complex interfaces
Our BeerRepository protocol has two pieces to it: The loadBeers()
method which you are now pretty familiar with, and a Combine publisher.
To learn more about the approach used here, wait for Part V: Interlude - implementing the Repository pattern with Combine.
We follow the same approach to put together our mock: We create a mock class that conforms to the interface, and we fill out the required methods and properties:
The
public init
is there as normalWe include a
stubLoadBeersResponse
to allow us to return either values or errors from our unit testsWe also count the number of times the
loadBeers()
method is called as expectedWe defer execution of an optional closure
didLoadBeers
in case we need to run any code after the responseFinally, the primary difference here is that instead of returning the
Result
value or error, we instead send the result to the publisher. This is part of the Repository pattern - you’ll see more of this in Part V: Interlude - implementing the Repository pattern with Combine!
Conclusion
You've made it, mock maestro! You now possess the skills you need to craft mocks for any API interface, system framework, or complex scenario.
With these powerful tools, you're ready to dive into the world of async testing. Buckle up and stay tuned for Part III - Async unit testing basics.
Happy mocking!
Part I Dependency Injection
Part II Mocking like a Pro
Part III Async unit testing basics
Part IV Advanced async testing - handling unstructured concurrency
Part V Interlude - implementing the Repository pattern with Combine
Part VI Combine, async/await, and unit testing