
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