Photo by NordWood Themes on Unsplash

Today, we are going to learn extra tips to test your ViewController.

It is always good to have extra test since it can increase your code validity. Testing your viewController can helps you be more extra aware of what is actually going on with the ViewController. There are some tips that I want to share to increase your code validity, so you won’t find this bug during the manual testing.

Testing storyboard instantiation correctness

We can add extra tests when instantiating a viewController from storyboard. Here, we can check that is our viewController that is instantiated from the storyboard with the specific viewController id has a correct type that is being set in the storyboard.

func test_init_returnCorrectInstanceType() {
    let storyboard = UIStoryboard(name: "MainViewController", bundle: .main)
    let viewController = storyboard.instantiateViewController(identifier: "MainViewController")

    XCTAssertTrue(viewController is MainViewController)
}

Testing instantiation does not perform any request

Sometimes, we don’t want to immediately perform something when you just instantiate an object. This, of course, to prevent unexpected behavior happens. When dealing with this scenario, we need to use a test double, that can knows how to capture event, or maybe a value. This is a special test double, called spy. So, we can use spy test double helper when instantiating our UIViewController instance, or SwiftUI’s View instance, upon its creation by inject the spy collaborator component. The idea is to make sure the collaborator receive empty event, or call count upon the SUT (System Under Tests) creation.

func test_init_doesNotPerformAnyRequestOnCreation() {
    let (_, viewModelSpy) = makeSUT()

    XCTAssertTrue(viewModelSpy.messages.isEmpty)
}

Testing IBOutlets connection

When dealing with Storyboard or Xibs, is it very common to use IBOutlet keyword to connect our view into code. But sometimes, the connection is lost because of the refactoring, or any other unexpected reason. We can improve our test by adding a new test, specifically to check wether the outlet properties is nil or not. In this case, if we run the test, we know immediately if there is a nil outlet.

func test_loadView_outletsShouldNotNil() {
    let sut = makeSUT()
    trackForMemoryLeaks(for: sut)

    sut.loadView()
    sut.viewDidLoad()

    XCTAssertNotNil(sut.tableView)
    XCTAssertNotNil(sut.activityIndicatorView)
}

Testing initial IBOutlets initial state

Testing initial state is also nice to have to always guarantee that our view is actually instantiated with correct state. With this test, we don’t need to debug, or even try it manually too often just to check the initial state upon the view creation.

func test_loadView_viewShouldInInitialState() {
    let sut = makeSUT()
    trackForMemoryLeaks(for: sut)

    sut.loadView()
    sut.viewDidLoad()

    XCTAssertNotNil(sut.tableView.visibleCells.isEmpty)
    XCTAssertTrue(!sut.activityIndicatorView.isAnimating)
    XCTAssertTrue(!sut.refreshControl.isRefreshing)
    XCTAssertEqual(sut.title, "Hi")
}

Conclusion

UI is the outer layer in the clean architecture’s onion diagram. Because of its nature (a framework), then it needs to run in a real or semi real environment, like simulator, or even a real device. It is actually an expensive and slow process to reach certain screen. That is why It is always nice to have extra tests to cover expected and unexpected behavior, specifically for UI level unit testing.

Reference

iOS Unit Testing by Example: XCTest Tips and Techniques Using Swift by Jon Reid

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s