You learned how to test your business logic in the interactor using Clean Swift in my last post. Naturally, the flow of control leads us to the presenter today.
Specifically, you’ll be able to expand the role of a spy to make sure a method is invoked with the expected argument. You’ll also write a rather interesting test case to make sure a date is formatted correctly.
We’ll finish off by swapping out the spy with a mock. You’ll see how easy that is.
Create the XCTestCase for the presenter
In Xcode, choose File -> New -> File, then select Test Case Class. Enter CreateOrderPresenterTests
for class. Make sure the CleanStoreTests target is checked. Click Create. The CreateOrderPresenterTests.swift
file is created.
The CreateOrderPresenter
’s input and output protocols are defined as follows:
1 2 3 4 5 6 7 8 9 10 |
protocol CreateOrderPresenterInput { func presentExpirationDate(response: CreateOrder_FormatExpirationDate_Response) } protocol CreateOrderPresenterOutput: class { func displayExpirationDate(viewModel: CreateOrder_FormatExpirationDate_ViewModel) } |
Apply the Single Responsibility Principle (SRP) to Test Case
There is only one method to test. Yay! But look at the CreateOrderPresenter
class’s presentExpirationDate()
method. It seems to do quite a bit here.
As you recalled from the last post, formatting data is the presenter’s primary responsibility. The presentExpirationDate()
method asks an NSDateFormatter
object to return an NSDate
in String
representation.
The dateFormatter
constant is set up outside of the method because we want to create it just once for efficiency reason. Later, we can reuse it for other formatting needs.
Even though it is not set up in the presentExpirationDate()
method, you still want to test it in some way. But because it isn’t a boundary method, you don’t want to test it directly. Maybe NSDateFormatter
is too slow, and you want to change the internal implementation in the future. So what do you do?
You’ll write two test methods to test the different aspects of the presentExpirationDate()
method.
testPresentExpirationDateShouldConvertDateToString()
The first thing the
presentExpirationDate()
method does is to ask theNSDateFormatter
to convert anNSDate
to aString
.-
testPresentExpirationDateShouldAskViewControllerToDisplayDateString()
The second thing the
presentExpirationDate()
method does is to create a view model with the string result and asks the view controller to display it.
You could write just one test method named testPresentExpirationDateShouldConvertDateToStringAndAskViewControllerToDisplayDateString()
and have two assertions. But I think it’s always good to make a method do just one thing. And test methods are also methods.
Setup and Spy
To test the presentExpirationDate()
method, you first need to set up the subject under test – CreateOrderPresenter
.
1 2 3 4 5 |
func setupCreateOrderPresenter() { createOrderPresenter = CreateOrderPresenter() } |
The CreateOrderPresenterOutputSpy
needs to spy on two things:
- The
displayExpirationDate()
method in the output protocol is invoked. - The
CreateOrder_FormatExpirationDate_ViewModel
argument contains the correctly formatted date string.
Here is the spy:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class CreateOrderPresenterOutputSpy: CreateOrderPresenterOutput { // MARK: Method call expectations var displayExpirationDateCalled = false // MARK: Argument expectations var createOrder_formatExpirationDate_viewModel: CreateOrder_FormatExpirationDate_ViewModel! // MARK: Spied methods func displayExpirationDate(viewModel: CreateOrder_FormatExpirationDate_ViewModel) { displayExpirationDateCalled = true createOrder_formatExpirationDate_viewModel = viewModel } } |
To make sure the displayExpirationDate()
method is invoked, you just need to set displayExpirationDateCalled
to true. You’ve done this before when writing tests for the interactor.
How do you make sure the CreateOrder_FormatExpirationDate_ViewModel
argument has the correctly formatted date string?
You don’t, in your spy. You do that in your assertion.
Here, you just want to save the argument to the createOrder_formatExpirationDate_viewModel
variable. You’ll inspect this variable in your assertion to verify the expiration date is formatted correctly.
Convert NSDate
to String
Enough forewords. Let’s get down to business. Let’s write a test to make sure the presenter can convert an NSDate
to String
in the exact format you desire.
Here is the test:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
func testPresentExpirationDateShouldConvertDateToString() { // Given let createOrderPresenterOutputSpy = CreateOrderPresenterOutputSpy() createOrderPresenter.output = createOrderPresenterOutputSpy let dateComponents = NSDateComponents() dateComponents.year = 2007 dateComponents.month = 6 dateComponents.day = 29 let date = NSCalendar.currentCalendar().dateFromComponents(dateComponents)! let response = CreateOrder_FormatExpirationDate_Response(date: date) // When createOrderPresenter.presentExpirationDate(response) // Then let returnedDate = createOrderPresenterOutputSpy.createOrder_formatExpirationDate_viewModel.date let expectedDate = "6/29/07" XCTAssertEqual(returnedDate, expectedDate, "Presenting an expiration date should convert date to string") } |
First, you instantiate the spy and set it to be the output of the presenter. You’ll later ask the spy some questions. No surprise here.
Next, you use NSDateComponents
and NSCalendar
to create an NSDate
(BTW, 2007/6/29 is the original iPhone release date).
You also want to prepare the parameter that is going to be passed to the method you’ll be testing. So you just instantiate the CreateOrder_FormatExpirationDate_Response
struct with the date you’ve just created.
Action! Invoke the presentExpirationDate()
method on createOrderPresenter
.
When it comes to assertions, it is always useful to first write down what you get and what you expect. You can ask the spy for the view model argument it recorded before and set it to returnedDate
. And “6/29/07” is the exact format you hope it matches. Set it to the expectedDate
constant.
Now, it is just a matter of comparing the two strings.
I find:
1 2 |
XCTAssertEqual(returnedDate, expectedDate, "Presenting an expiration date should convert date to string") |
to be much more readable than:
1 2 |
XCTAssertEqual(createOrderPresenterOutputSpy.createOrder_formatExpirationDate_viewModel.date, "6/29/07", "Presenting an expiration date should convert date to string") |
If the test fails in the future, you can read the description in the assert statement to get a context, before you try to figure out what the returned and expected results are, and why they don’t match.
But of course our test passes.
Display Date String
The second test looks similar to the testFormatExpirationDateShouldAskPresenterToFormatExpirationDate()
method of the CreateOrderInteractorTests
you’ve seen in my last post.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
func testPresentExpirationDateShouldAskViewControllerToDisplayDateString() { // Given let createOrderPresenterOutputSpy = CreateOrderPresenterOutputSpy() createOrderPresenter.output = createOrderPresenterOutputSpy let response = CreateOrder_FormatExpirationDate_Response(date: NSDate()) // When createOrderPresenter.presentExpirationDate(response) // Then XCTAssert(createOrderPresenterOutputSpy.displayExpirationDateCalled, "Presenting an expiration date should ask view controller to display date string") } |
Again, you ask the spy to get on another mission.
This time, though, since you’re only interested in whether the view controller’s displayExpirationDate()
method is getting called, you can just create the CreateOrder_FormatExpirationDate_Response
struct with any date, like NSDate()
.
You don’t care about the format of the date, nor its value. You already covered correctness of your presentation logic in the first test you wrote. This is SRP in action.
Invoke the presentExpirationDate()
method and assert displayExpirationDateCalled
is true.
What does the whole test look like?
The finished test case looks like:
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 |
class CreateOrderPresenterTests: XCTestCase { // MARK: Subject under test var createOrderPresenter: CreateOrderPresenter! // MARK: Test lifecycle override func setUp() { super.setUp() setupCreateOrderPresenter() } override func tearDown() { super.tearDown() } // MARK: Test setup func setupCreateOrderPresenter() { createOrderPresenter = CreateOrderPresenter() } // MARK: Test doubles class CreateOrderPresenterOutputSpy: CreateOrderPresenterOutput { // MARK: Method call expectations var displayExpirationDateCalled = false // MARK: Argument expectations var createOrder_formatExpirationDate_viewModel: CreateOrder_FormatExpirationDate_ViewModel! // MARK: Spied methods func displayExpirationDate(viewModel: CreateOrder_FormatExpirationDate_ViewModel) { displayExpirationDateCalled = true createOrder_formatExpirationDate_viewModel = viewModel } } // MARK: Test expiration date func testPresentExpirationDateShouldConvertDateToString() { // Given let createOrderPresenterOutputSpy = CreateOrderPresenterOutputSpy() createOrderPresenter.output = createOrderPresenterOutputSpy let dateComponents = NSDateComponents() dateComponents.year = 2007 dateComponents.month = 6 dateComponents.day = 29 let date = NSCalendar.currentCalendar().dateFromComponents(dateComponents)! let response = CreateOrder_FormatExpirationDate_Response(date: date) // When createOrderPresenter.presentExpirationDate(response) // Then let returnedDate = createOrderPresenterOutputSpy.createOrder_formatExpirationDate_viewModel.date let expectedDate = "6/29/07" XCTAssertEqual(returnedDate, expectedDate, "Presenting an expiration date should convert date to string") } func testPresentExpirationDateShouldAskViewControllerToDisplayDateString() { // Given let createOrderPresenterOutputSpy = CreateOrderPresenterOutputSpy() createOrderPresenter.output = createOrderPresenterOutputSpy let response = CreateOrder_FormatExpirationDate_Response(date: NSDate()) // When createOrderPresenter.presentExpirationDate(response) // Then XCTAssert(createOrderPresenterOutputSpy.displayExpirationDateCalled, "Presenting an expiration date should ask view controller to display date string") } } |
You could also extract the Given and When because both test methods set up the spy and invoke the same method under test.
But I chose not to, because I think every test case should have a clearly defined Given, When, and Then. When you extract too much and just have an assert in the test method, you start to lose context of why you write the tests in the first place.
That’s largely a personal taste, but these things come in alphabetical order:
- DCO – Don’t Confuse Others
- DCY – Don’t Confuse Yourself
- DRY – Don’t Repeat Yourself
You don’t want to be 007?
“I don’t like to spy on people. Save that for another James Bond movie. I just want to mock people.”
I hear ya. So here’s a little side adventure.
You’ve seen how to use a spy as a stand-in for the actual thing. A spy works by abstracting away the details of an object/class/component. You can then focus on the test on hand. But a spy reports to the boss who determines if a test passes or fails.
Let’s turn the spy into a mock.
Mock = Spy + Verify
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 |
class CreateOrderPresenterOutputMock: CreateOrderPresenterOutput { // MARK: Method call expectations var displayExpirationDateCalled = false // MARK: Argument expectations var createOrder_formatExpirationDate_viewModel: CreateOrder_FormatExpirationDate_ViewModel! // MARK: Spied methods func displayExpirationDate(viewModel: CreateOrder_FormatExpirationDate_ViewModel) { displayExpirationDateCalled = true createOrder_formatExpirationDate_viewModel = viewModel } // MARK: Verifications func verifyDisplayExpirationDateIsCalled() -> Bool { return displayExpirationDateCalled } func verifyExpirationDateIsFormattedAs(date: String) -> Bool { return createOrder_formatExpirationDate_viewModel.date == date } } |
Forgot about the differences between test doubles? Read The Swifty Little Mocker.
In a gist,
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.
To add behavior verifications, you add the following methods:
verifyDisplayExpirationDateIsCalled()
to make suredisplayExpirationDate()
is calledverifyExpirationDateIsFormattedAs()
to make sure the date strings match.
Then just return a Bool
as a result so you can use the simplest XCTAssert()
.
Let’s swap out the spy and swap in the mock in our tests.
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 |
func testPresentExpirationDateShouldConvertDateToString() { // Given let createOrderPresenterOutputMock = CreateOrderPresenterOutputMock() createOrderPresenter.output = createOrderPresenterOutputMock let dateComponents = NSDateComponents() dateComponents.year = 2007 dateComponents.month = 6 dateComponents.day = 29 let date = NSCalendar.currentCalendar().dateFromComponents(dateComponents)! let response = CreateOrder_FormatExpirationDate_Response(date: date) // When createOrderPresenter.presentExpirationDate(response) // Then let expectedDate = "6/29/07" XCTAssert(createOrderPresenterOutputMock.verifyExpirationDateIsFormattedAs(expectedDate), "Presenting an expiration date should convert date to string") } func testPresentExpirationDateShouldAskViewControllerToDisplayDateString() { // Given let createOrderPresenterOutputMock = CreateOrderPresenterOutputMock() createOrderPresenter.output = createOrderPresenterOutputMock let response = CreateOrder_FormatExpirationDate_Response(date: NSDate()) // When createOrderPresenter.presentExpirationDate(response) // Then XCTAssert(createOrderPresenterOutputMock.verifyDisplayExpirationDateIsCalled(), "Presenting an expiration date should ask view controller to display date string") } |
Do you like to spy or mock people?
Where can I find the code?
I’ve included the complete test case with both spy and mock on GitHub. The repo serves as a companion to this journey of testing. I commit at every step of the way so you can see how the code has evolved.
In a future post, when we use TDD to drive a feature, you’ll be able to see the exact thought process I use to arrive at the final implementation with tests. I encourage you to check it out, follow or star it.
I can’t wait to see how you test the view controller
Check out the repo!
I’ve already started to test the CreateOrderViewController
component. It’s 90% complete. You can get a sneak peak if you clone the repo and look at CreateOrderViewControllerTests.swift
.
The view controller is going to be the star of the show. Every iOS developer’s dream is to kill massive view controllers in their code. It’s going to be as epic as the first post.
If you’ve enjoyed this series so far, can you do me a favor by sharing this post with your peers on Twitter, Facebook, LinkedIn, or email? That way, more iOS developers can benefit from using Clean Architecture and writing tests for their apps. Ultimately, we’ll all benefit from seeing more well written and tested code than bad code.
You can find the full source code at GitHub.
Hi Raymond,
Its really awesome to follow your posts one after the other and getting more clarity with regards to Clean Architecture as I go along. Thanks a lot for all your posts and keep up the good work !!!
I have a question related to Spy and Mocks. Could you please elucidate as to when should one be preferred over the other and citing severals more benefits of each? Thanks.
Hi Arpit,
I wrote a detailed post to explain the difference between all kinds of test doubles.
http://clean-swift.com/swifty-little-mocker/
In short, a spy records what happens – which method is called and with what arguments. In your test, you need to interrogate a spy to assert the things you expect to happen do in fact happen.
With a mock, the assertion is built in – usually a
verify()
method. In your test, you just need to callverify()
. If it returns true, you’re done.It mostly comes down to taste but you may want to be consistent in your project.