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
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.
class ListGistsViewControllerTests: XCTestCase
// MARK: Subject under test
var sut: ListGistsViewController!
var window: UIWindow!
// MARK: Test lifecycle
override func setUp()
window = UIWindow()
override func tearDown()
window = nil
// MARK: Test setup
let bundle = Bundle.main
let storyboard = UIStoryboard(name: "Main", bundle: bundle)
sut = storyboard.instantiateViewController(withIdentifier: "ListGistsViewController") as! ListGistsViewController
// MARK: Test doubles
class ListGistsBusinessLogicSpy: ListGistsBusinessLogic
var fetchGistsCalled = false
func fetchGists(request: ListGists.FetchGists.Request)
fetchGistsCalled = true
// MARK: Tests
let listGistsBusinessLogicSpy = ListGistsBusinessLogicSpy()
sut.interactor = listGistsBusinessLogicSpy
XCTAssertTrue(listGistsBusinessLogicSpy.fetchGistsCalled, "viewDidLoad() should ask the interactor to fetch gists")
// 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
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
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.
You’re probably already familiar with the
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.
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.
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.
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.
The test methods are of the format
testXXX(). Every method in a
XCTestCase subclass that starts with
test will be executed between the
Now, let’s dive deeper into the
testShouldFetchGistsWhenViewIsLoaded() test method. It has three parts:
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.