In my last post, I wanted to focus on showing you how Clean Swift works. So I didn’t write any test. You saw how Clean Swift can help you organize your codebase, help you write clean code, and find things later. You learned how to extract your business logic into the interactor and presentation logic into the presenter in the VIP cycle. Finally, you saw how Swift protocols help you decouple your objects.
This post focuses on these protocols that result in a side (or rather main) benefit: Much easier unit testing. Without tests, we can’t be confident the changes we make do not affect existing code.
But first thing first.
Based on Uncle Bob’s The Little Mocker, the following is a short introduction to the different kinds of test objects – in Swift. I highly recommend you read his funny yet educational post.
Java interface == Swift protocol
1 2 3 4 5 6 7 8 |
interface Authorizer { public Boolean authorize(String username, String password); } protocol Authorizer { func authorize(username: String, password: String) -> Bool } |
Nuff said.
Test double
The name test double refers to the whole family of objects that are used in tests.
Dummy
You pass a dummy as an argument to a function when you don’t care how it’s used. As part of a test, when you must pass an argument, but you know the argument will never be used in the method being tested, you pass a dummy. Since a dummy is never used, it can return anything or nothing. Returning nil is perfectly acceptable and most logical.
1 2 3 4 5 6 7 8 9 10 11 12 |
public class DummyAuthorizer implements Authorizer { public Boolean authorize(String username, String password) { return null; } } class DummyAuthorizer: Authorizer { func authorize(username: String, password: String) -> Bool? { return nil } } |
DummyAuthorizer
is a test dummy and returns nil because we don’t really care.
An example test
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
public class System { public System(Authorizer authorizer) { this.authorizer = authorizer; } public int loginCount() { //returns number of logged in users. } } @Test public void newlyCreatedSystem_hasNoLoggedInUsers() { System system = new System(new DummyAuthorizer()); assertThat(system.loginCount(), is(0)); } class System { var authorizer: Authorizer init(authorizer: Authorizer) { self.authorizer = authorizer } func loginCount() -> Int { //returns number of logged in users. return 0 } } class TheLittleMocker: XCTestCase { func testNewlyCreatedSystem_hasNoLoggedInUsers() { let system = System(authorizer: DummyAuthorizer()) XCTAssert(system.loginCount() == 0, "Pass") } } |
We want to test a newly created system has no logged-in users. System
is a class whose initializer must take an Authorizer
as argument to determine permissions. But since our goal is to make sure a new user should have a zero login count, we don’t care about permissions or the Authorizer
that we must pass in. The DummyAuthorizer
is perfect for this case because it is so dumb that it doesn’t even know what username and password mean.
Stub
A stub is a dummy that returns a specific value because the rest of the system relying on this specific value to continue running in a test.
1 2 3 4 5 6 7 8 9 10 11 12 |
public class AcceptingAuthorizerStub implements Authorizer { public Boolean authorize(String username, String password) { return true; } } class AcceptingAuthorizerStub: Authorizer { func authorize(username: String, password: String) -> Bool? { return true } } |
If the rest of the System
class relies on the authorize()
method returning true in order to continue, we can make AcceptingAuthorizerStub
to always return true. We don’t care what the username and password are, we are just going to log anyone in. Our goal is to test the login count after a successful login. We’ll define what a successful login means in another test.
Spy
You use a spy when you want to make sure a method is called in your test. It can spy on more stuff, such as how many times a method is called and what arguments are passed in. But you have to be careful. The more stuff you spy, the tighter you couple your tests to the implementation of your app. Coupling leads to fragile tests.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public class AcceptingAuthorizerSpy implements Authorizer { public boolean authorizeWasCalled = false; public Boolean authorize(String username, String password) { authorizeWasCalled = true; return true; } } class AcceptingAuthorizerSpy: Authorizer { var authorizeWasCalled = false func authorize(username: String, password: String) -> Bool? { authorizeWasCalled = true return true } } |
The AcceptingAuthorizerSpy
keeps an authorizeWasCalled
boolean. When the authorize()
method is called, it records that fact. In your test, during your assertion, you can verify the invocation indeed happened.
Mock
A mock is similar to a spy and does a little more. It also tests behavior by having the assertion go into the mock itself. A mock is not so interested in the return values of functions. It’s more interested in what function were called, with what arguments, when, and how often.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
public class AcceptingAuthorizerVerificationMock implements Authorizer { public boolean authorizeWasCalled = false; public Boolean authorize(String username, String password) { authorizeWasCalled = true; return true; } public boolean verify() { return authorizeWasCalled; } } class AcceptingAuthorizerVerificationMock: Authorizer { var authorizeWasCalled = false func authorize(username: String, password: String) -> Bool? { authorizeWasCalled = true return true } func verify() -> Bool { return authorizeWasCalled } } |
The AcceptingAuthorizerVerificationMock
’s verify()
method returns true only if the method invocation happens. Instead of peeking inside a spy in your assertion, you call the mock’s verify()
method to assert it is true. A spy spies on citizens because they won’t behave without authority, whereas a mock is self policing.
Fake
So far, all the test objects you’ve seen don’t care about what arguments you pass in. Even a mock only records the arguments, it doesn’t use the arguments to generate a different return value. However, a fake has specific business logic. You can drive its behavior by passing in different arguments to make it return different results.
1 2 3 4 5 6 7 8 9 10 11 12 |
public class AcceptingAuthorizerFake implements Authorizer { public Boolean authorize(String username, String password) { return username.equals("Bob"); } } class AcceptingAuthorizerFake: Authorizer { func authorize(username: String, password: String) -> Bool? { return username == "Bob" } } |
When you are testing a certain business rule (for example, a user can only log in with correct username), you use AcceptingAuthorizerFake
. The authorize()
method returns true only if the username is ‘Bob.’
The whole enchilada
The following is a fully functional test written in Swift using Xcode’s default XCTest. You can copy and paste into a new file in Xcode and it’ll pass.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
import UIKit import XCTest protocol Authorizer { func authorize(username: String, password: String) -> Bool? } class DummyAuthorizer: Authorizer { func authorize(username: String, password: String) -> Bool? { return nil } } class AcceptingAuthorizerStub: Authorizer { func authorize(username: String, password: String) -> Bool? { return true } } class AcceptingAuthorizerSpy: Authorizer { var authorizeWasCalled = false func authorize(username: String, password: String) -> Bool? { authorizeWasCalled = true return true } } class AcceptingAuthorizerVerificationMock: Authorizer { var authorizeWasCalled = false func authorize(username: String, password: String) -> Bool? { authorizeWasCalled = true return true } func verify() -> Bool { return authorizeWasCalled } } class AcceptingAuthorizerFake: Authorizer { func authorize(username: String, password: String) -> Bool? { return username == "Bob" } } class System { var authorizer: Authorizer init(authorizer: Authorizer) { self.authorizer = authorizer } func loginCount() -> Int { return 0 } } class TheLittleMocker: XCTestCase { func testNewlyCreatedSystem_hasNoLoggedInUsers() { let system = System(authorizer: DummyAuthorizer()) XCTAssert(system.loginCount() == 0, "Pass") } } |
In the testNewlyCreatedSystem_hasNoLoggedInUsers()
method, you can swap in any of the test objects and the test still passes. The key takeaway is this. As the System
class grows and the Authorizer
being used differently in its implementation, you also need to make your test double more sophisticated to make sure the test continue to pass. This means your original DummyAuthorizer
may need to evolve into a stub, spy, mock, or fake.
That’s it for the Swifty Little Mocker.
What can you do with the Swifty Little Mocker?
In a future post, I’ll show you the entire TDD approach from start to finish. That is, given the requirements, how can you write a test first to drive the feature implementation.
Better yet, you’ll use what you’ve learned in this post to write your own mocks and stubs. You won’t need or want to learn a new testing or mocking framework because you’ll see writing your own is so much easier. No more worries about a CocoaPod breaking your build or keeping up to date with extra frameworks.
Best yet, your tests will be blazingly fast because there is no external frameworks to import and load. You only need to test your boundary methods, not private methods. You’ll see why and how.
If this sounds interesting to you, make sure you subscribe below so you don’t miss out. As a bonus, you’ll also get my Xcode templates so you can start using the Clean Swift architecture in your iOS projects today.
In the next post, you’ll learn what unit testing really mean? Do you really need to test every single method of every class? How do you make sure every code path is covered? How do you avoid writing fragile tests?
Stay tuned.
Getting the following compile error on XCTAssertTrue
After I added the following test case to test the “Spy” scenario
Hi Randy,
Is
authorizeWasCalled
defined in yourAcceptingAuthorizerSpy
class?Got the error:
Type ‘DummyAuthorizer’ does not conform to protocol ‘Authorizer’
The method authorize should return non-nil value.