Testing Presentation Logic in Presenter

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:

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.

  1. testPresentExpirationDateShouldConvertDateToString()

    The first thing the presentExpirationDate() method does is to ask the NSDateFormatter to convert an NSDate to a String.

  2. 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.

The CreateOrderPresenterOutputSpy needs to spy on two things:

  1. The displayExpirationDate() method in the output protocol is invoked.
  2. The CreateOrder_FormatExpirationDate_ViewModel argument contains the correctly formatted date string.

Here is the spy:

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:

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:

to be much more readable than:

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.

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:

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

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 sure displayExpirationDate() is called
  • verifyExpirationDateIsFormattedAs() 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.

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.

Get the Clean Swift Xcode Templates

Subscribe below to get my Xcode templates and learn how to apply the VIP cycle to your projects, extract business and presentation logic into interactor and presenter, navigate to different scenes using multiple storyboards, and write fast, maintainable tests with confidence to make changes.

I promise I'll never send you spam. You can unsubscribe at any time.

Raymond
I've been developing in iOS since the iPhone debuted, jumped on Swift when it was announced. Writing well-tested apps with a clean architecture has been my goal.

2 Comments

  1. 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.

    1. 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 call verify(). If it returns true, you’re done.

      It mostly comes down to taste but you may want to be consistent in your project.

Leave a Comment

Your email address will not be published. Required fields are marked *