How to test your viewDidLoad() method

Even if you’ve been an iOS developer for one single day, you must’ve come across the situation where you need to do something when the viewDidLoad() method is called. Okay, easier enough. Just put some code in there.

But how do you test it? How can you ensure the viewDidLoad() method is called – your view is fully loaded – before your view controller tests execute?

This is very important. If viewDidLoad() is not called before your unit tests run, all tests fail. Nothing happens. You may also find that your tests sometimes pass, sometimes fail. And you have no idea why. This is likely a timing issue. The viewDidLoad() is called, but before some of your tests execute, and after some of your tests execute.

Read on to find out how to reliably test your view controller and its viewDidLoad() method.

Unit test template

I’m going to show you my basic unit test setup for view controllers, and then explain each line of code below.

I use // MARK: to organize sections of code in the same source file. It’s a great facility by Xcode to provide a convenient pulldown menu to jump to the relevant section. There’s also // MARK: -. The extra - tells Xcode to add a line in that pulldown menu. It’s an awesome way to add a one-level hierarchy to separate the higher- and lower- level sections.

Subject under test

The sut variable stands for subject under test. Its methods and behaviors are being tested in this test class. So the ListGistsViewControllerTests class tests the methods of ListGistsViewController.

In an iOS app, any UIView or its subclasses must be attached to another view. And UIWindow provides a container for the root view. The window variable is used to act as this container so that your view controller under test can attach its view onto.

Test lifecycle

You’re probably already familiar with the setUp() and tearDown() methods. For each test method in the test class, the setUp() method is executed before, and the tearDown() method is executed after. The setUp() method creates a new instance of UIWindow, and the tearDown() method releases it. It also calls the setupListGistsViewController() method to create a new test subject. This setup ensures each test in the class is completely isolated and doesn’t carry any left-over states from a previous test that might affect the results of the current test.

Test setup

The setupListGistsViewController() method creates a new instance of ListGistsViewController from the storyboard in the main bundle. If you don’t use storyboard, you can easily change this to instantiate the test subject from code.

In the loadView() method, we first add the view controller’s view as a subview to the window’s view hierarchy. However, this alone is not enough. The high resolution display also means it takes time to render and draw the view on the screen. The drawing operations would have taken up all the CPU cycles, thus blocking and slowing down everything else. The run loop is used to manage these I/O sources to allow them sufficient time to be processed.

When running unit tests, we do really want to wait for the view to be ready. Because if an UIButton isn’t drawn, you can’t tap on it. Your test would have no effect. The RunLoop.current.run(until: Date()) statement makes sure the run loop associated with the current thread has ample time to let the drawing operations to complete. After the loadView() method finishes, your view controller is ready to be tested.

One thing to note is that we don’t invoke the loadView() method during test setup. You’ll see why shortly.

You can read more about run loops.

Test doubles

This section contains any test doubles that I need to support my tests’ Given. All your stubs and mocks should go here. In this particular example, the ListGistsBusinessLogicSpy is defined to spy on the fetchGists(request:) method. The fetchGistsCalled Boolean variable is used to record the method invocation. When the method is called, it simply sets it to true.

Test Method

The test methods are of the format testXXX(). Every method in a XCTestCase subclass that starts with test will be executed between the setUp() and tearDown().

Now, let’s dive deeper into the testShouldFetchGistsWhenViewIsLoaded() test method. It has three parts:

  1. Given
  2. When
  3. Then

In Given, you set up any test doubles that you need in your test. When do you need a test double? You use a test double to replace a real dependency. The real dependency does things silently and doesn’t tell you if it does or does not. So you substitute a fake one – the test double. It’s free for you to change. For example, you can override a method to make it does nothing, but only tell you it was invoked. This is essentially why we use test doubles in unit testing. We don’t want the dependency to do the real thing. We just want to know it was asked to do the thing. But instead of leaving the method empty, we can set a Boolean variable form false to true. At the beginning this variable is set to false. At the end of the test, we check this variable again. If it’s true, the method has been called during the test. In the example above, we set up the ListGistsBusinessLogicSpy test double in Given.

In When, you simply invoke the method on the test subject. If the method requires arguments or some initial states. You set these up in Given. One thing to note is this. When you’re testing the viewDidLoad() method, you’re interested in the things that happen within that method. But calling loadView() will have already triggered viewDidLoad() before your test setup code in Given has a chance to run. So we want to invoke loadView() in When. For any other tests, you can simply invoke the loadView() method in Given. In the example above, we invoke the loadView() method on the view controller in When.

In Then, you write assertion statements to check the states of the test doubles and test subject to make sure the desired behaviors are observed. In the example above, we assert that listGistsBusinessLogicSpy.fetchGistsCalled is set to true.

So that’s it. You can use this simple template to test all your view controllers in your app.

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.

Leave a Comment

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