Unit Test - UICollectionView in UIViewController with example
Testing is an important aspect of development. No matters if you belong to the Test Driven Development(TDD) group or not, one should never ignore tests.
Beginning from Xcode 5, Apple introduce the XCTest Framework. Performance measurement was then introduce on Xcode 6 and UI Testing on Xcode 7. However, unit testing a UIViewController
is still difficult. This is mainly due to the lifecycle and the way the app module is setup.
Unit test or UITest UIViewController
?
Unit test aim to test small portions of our code during development while UI Test tend to only be implemented at the end like a integration test. Also, unit test run a lot faster compare to UI test. I would say, you need both to have a more robust code base.
But unit testing UIViewController
is difficult
Yes, is difficult. We have lot of things to take care of before having a suitable test environment. However, difficult does not equate to undoable. We can still write tests or even do TDD for it. These would also help us to understand the lifecycle of UIViewController
better.
Usual Set up
This is what I have in most of my UIViewController
tests.
import UIKit
import XCTest
@testable import MyPhotoProject
class PhotoViewControllerTests: XCTestCase {
var viewController: PhotoViewController!
override func setUp() {
super.setUp()
//1
let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)
let navigationController = storyboard.instantiateInitialViewController() as! UINavigationController
viewController = navigationController.topViewController as! PhotoViewController
//2
UIApplication.shared.keyWindow!.rootViewController = viewController
//3
XCTAssertNotNil(navigationController.view)
XCTAssertNotNil(viewController.view)
}
}
Number 1 is the setup to create the ViewController
that we want to test. If on another app that are using UITabBarController
, we can replace the navigationController
with it. We can also use instantiateViewController(withIdentifier:)
if needed. I usually use a framework called R.swift. It very handy to access viewController
, image
or identifier
that is already in the project.
2 just like what our AppDelegate
do, we set it to the rootViewController
when the setup is run. This is really important if we are testing views related stuff like UICollectionView
.
3 help us to prepare the views with IBOutlet
and IBAction
connection that is setup via storyboard. I usually use _ = controller.View
but have recently change to these after reading it on NatashaTheRobot blog post.
Testing a UICollectionView
So with the above setup, a sample test of UICollectionView
in a ViewController
is as follows:
func testCollectionViewCellsIsDisplayedWithMatchingImage() {
//1 Arrange
let fakeImagesName = ["FakeA", "FakeB", "FakeC"]
viewController.imagesName = fakeImagesName
//2 Act
viewController.collectionView.reloadData()
RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.5))
//3 Assert
let cells = viewController.collectionView.visibleCells as! [PhotoCollectionViewCell]
XCTAssertEqual(cells.count, fakeImagesName.count, "Cells count should match array.count")
for I in 0...cells.count - 1 {
let cell = cells[I]
XCTAssertEqual(cell.photoImageView.image, UIImage(named: fakeImagesName[I]), "Image should be matching")
}}
At 1, we Arrange the fake data required for the test.
2, we Act on it. We called the reloadData()
to trigger the collectionView
delegate. The RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.5))
is required as a “hacky” way of waiting for the data to be loaded.
We then Assert on 3. These is where all the checking is done. Here, we ensure that the count on the cells
is correct and each cell.photoImageView.image
is equal to the UIImage
that it should be displaying.
Quick and Nimble
Quick are a testing framework framework for Swift
and Objective-C
. Using it together with Nimble, a matcher framework would help us to write better tests and have less boilerplate code.
With the above example, the test would look like this with Quick
and Nimble
.
class PhotoViewControllerSpec: QuickSpec {
override func spec() {
describe("A PhotoViewController") {
var viewController: PhotoViewController!
beforeEach {
let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)
let navigationController = storyboard.instantiateInitialViewController() as! UINavigationController
viewController = navigationController.topViewController as! PhotoViewController
let window = UIWindow(frame: UIScreen.main.bounds)
window.makeKeyAndVisible()
window.rootViewController = viewController
_ = viewController.view
}
context("should be displaying photo on cells") {
it("should match datasource count and display correct UIImage") {
let fakeImagesName = ["coffee1","coffee2","coffee3","coffee4","coffee5","coffee6"]
viewController.imagesName = fakeImagesName
viewController.collectionView.reloadData()
waitUntil { done in
if let cells = viewController.collectionView.visibleCells as? [PhotoCollectionViewCell] {
expect(cells).to(haveCount(fakeImagesName.count))
for i in 0...cells.count - 1 {
let cell = cells[i]
expect(cell.photoImageView.image).to(equal(UIImage(named: fakeImagesName[i])))
}
done()
}
}
}
}
}
}
}
Using Quick
and Nimble
helps us to reduce the Arrange code significantly as the ViewController
get larger. It also help us to write better tests with expect
instead of XCTAssert
.
In the case above, instead of using RunLoop
, we can use the waitUntil
for the action to complete. We also replace all the XCTAssert
with expect
. Looking at the expect(cells).to(haveCount(fakeImagesName.count))
, it read very much like a English sentence.
Conclusion
Unit testing a UIViewController
is certainly doable. As times goes by, Apple would surely improve the XCTest Framework
. Covering our code with more tests cases would surely help make the app more robust.