Monday, 13 March 2017

Common unit testing techniques on iOS

TL;DR - Most if not all unit test cases on iOS can follow the same commonly known pattern: GIVEN a set of initial conditions, WHEN something happens, THEN something is expected.
Note: Code snippets are written in Swift 3 using XCTest assertions

Preamble: Key definitions

The structure of a unit test almost always follows this pattern:
  1. Given a set of initial conditions
  2. When something happens
  3. Then something is expected
The object being tested is generally referred as the system under test (SUT). Objects that interact with the SUT and are needed to be able to write a unit test are called test doubles. I'll use the term mock instead of test double since it's more commonly used even though technically it's not correct. Swift does not have mocking frameworks because reflection is limited and for pure swift objects it's not possible to change the implementation of methods. Therefore, in Swift we need to implement our mocks manually. A real object can be substituted by a mock using dependency injection.

Typical kind of unit tests

Unit testing in iOS has come a long way since the iOS SDK was released in 2008. It's no longer rare to see iOS developers unit testing the majority of the code they write. The following are some of the most common cases for unit tests:
  1. Assert a method returns an expected value given:
  2. Assert properties instantiated depending on parameters
  3. Assert a method in a mock gets called
  4. Assert that calling a method in the SUT has a side effect in:
  5. Assert that a change in a (mocked) dependency has a side effect in the SUT

Unit Test Examples

1a. Assert a method returns an expected value given an input:

A method that returns the element in a array if it exists could look something like this:
extension Array {
    subscript (safe index: Int) -> Element? {
        return indices ~= index ? self[index] : nil
    }
}
The unit tests for this method can be done by treating the SUT as a black box. The following test class shows how three test cases can be written to test the behaviour of the safe index function. The code is intentionally verbose to demonstrate the example better.
class ArrayTests: XCTestCase {

    func testIndexWithinBoundsReturnsElement() {
        // GIVEN
        let sut = [1, 2, 3]
        
        // WHEN
        let itemAtIndex = sut[safe: 1]
        
        // THEN
        XCTAssertEqual(itemAtIndex, 2)
    }
    
    func testNegativeIndexReturnsNil() {
        let sut = [1, 2, 3]
        let itemAtIndex = sut[safe: -1]
        XCTAssertNil(itemAtIndex)
    }
    
    func testOutOfboundsIndexReturnsNil() {
        let sut = [1, 2, 3]
        let itemAtIndex = sut[safe: 3]
        XCTAssertNil(itemAtIndex)
    }
}

1b. Assert a method returns an expected value given the state of a dependency

The business logic for deciding whether to show an alert to the user requesting for location permissions could be implemented like this:
struct OnboardingState_Untestable {
    func shouldShowEnableLocationAlert() -> Bool {
        guard CLLocationManager.locationServicesEnabled() else { return true }
        return false
    }
}
The problem of this implementation is that the shouldShowEnableLocationAlert method uses CLLocationManager internally to return it's output. Therefore, to test this method we need mock and inject this dependency. Defining a protocol that CLLocationManager can automatically conform to:
protocol LocationManagerType: class {
    static func locationServicesEnabled() -> Bool
}
extension CLLocationManager: LocationManagerType {}
Modifying shouldShowEnableLocationAlertto pass the LocationManagerType type as a parameter defaulting to the CLLocationManagerwill allow to mock the dependency in a unit test. Note that this does not affect how this method is used in production. This kind of dependency injection is called Interface injection and in this case we are injecting a type as opposed to an instance because the CLLocationManager method we need to mock is a class function. The updated OnboardingStatewould be:
struct OnboardingState {
    func shouldShowEnableLocationAlert(locationManager: LocationManagerType.Type = CLLocationManager.self) -> Bool {
        guard locationManager.locationServicesEnabled() else { return true }
        return false
    }
}
Mocking a class function as opposed to an instance function is generally messier because classes aresingletons. There is only one class definition per instance of a program. Hence, the mocked state needs to be global. Using protocol conformance we can mock the CLLocationManager:
class MockedLocationManager: LocationManagerType {
    static var mockedLocationServicesEnabled = true
    static func locationServicesEnabled() -> Bool {
        return mockedLocationServicesEnabled
    }
}
A subclass of XCTestCase can override the setUp and tearDown methods. These functions get called before and after each test. It's common to initialise the SUT on setUp but not necessary. It's good practice to implement the tearDown method to deinitialise your SUT and mocks, specially the reference type ones. Otherwise, these objects will continue to exists while other tests run potentially interfering with them. This post explains possible issues in more detail. The 2 test cases for the OnboardingState can be:
class OnboardingStateTests: XCTestCase {
    
    var sut: OnboardingState!
    
    override func setUp() {
        super.setUp()
        // GIVEN
        sut = OnboardingState()
    }
    
    override func tearDown() {
        sut = nil
        super.tearDown()
    }
    
    func test_shouldShowEnableLocationAlert_returnsTrue_when_locationServicesAreDisabled() {
        // WHEN
        MockedLocationManager.mockedLocationServicesEnabled = false

        // THEN
        XCTAssert(sut.shouldShowEnableLocationAlert(locationManager: MockedLocationManager.self))
    }
    
    func test_shouldShowEnableLocationAlert_returnsFalse_when_locationServicesAreEnabled() {
        MockedLocationManager.mockedLocationServicesEnabled = true
        XCTAssertFalse(sut.shouldShowEnableLocationAlert(locationManager: MockedLocationManager.self))
    }
}

2. Assert properties instantiated depending on parameters

Let's say that a person is represent by the following struct and the JSON parsing is implemented in a similar way as Apple explains.
struct Person {
    let name: String
    let age: Int
}

extension Person {
    init?(dictionary: [String: Any]) {
        guard let name = dictionary["name"] as? String,
              let age = dictionary["age"] as? Int else {
                return nil
        }
        self.name = name
        self.age = age
    }
}
One of the tests would be to assert that for a given valid dictionary a valid Person is created. Personally, I don't mind having more than one assertion in a test if they are related. For example, the following test asserts both the name and age properties:
class PersonTests: XCTestCase {

    func test_allPropertiesAreSetCorrectlyForAValidDictionary() {
        // GIVEN
        let validDictionary: [String: Any] = ["name": "John Doe", "age": 35 ]
        
        // WHEN
        let sut = Person(dictionary: validDictionary)
        
        // THEN
        XCTAssertEqual(sut?.name, "John Doe")
        XCTAssertEqual(sut?.age, 35)
    }
}

3. Assert a method in a mock gets called

Commonly we need to download images from a URL, for example when displaying them in a UITableView. This task can be done by the following ImageFetcher which uses an OperationQueue and adds some kind of image operation that takes a URL and performs a network request to retrieve it. If we pop this screen we probably want to cancel all current image operations as they are no longer relevant. To do this we call the cancelAllOperations method on the queue.
class ImageFetcher {
    
    private let operationQueue: OperationQueue
    init(operationQueue: OperationQueue = OperationQueue()) {
        self.operationQueue = operationQueue
    }
    
    func fetch(imageURL: URL, completion: (UIImage?) -> Void) {
        // some implementation adding an image operation to the queue
    }
    
    func cancelFetchingAllImages() {
        operationQueue.cancelAllOperations()
    }
}
To be able to test this we pass the OperationQueue in the initialiser, this is know as Constructor injection. Using inheritance we can mock the OperationQueue and count the number of times cancelAllOperations is called. In our test case we can assert that cancelAllOperations gets called exactly once when cancelFetchingAllImages is called.
class MockOperationQueue: OperationQueue {
    var cancelAllOperationsCountCallCount = 0
    override func cancelAllOperations() {
        // In this case super is called to avoid having
        // side effects that are not true
        super.cancelAllOperations()
        cancelAllOperationsCountCallCount += 1
    }
}

class ImageFetcherTests: XCTestCase {
    
    var sut: ImageFetcher!
    var mockOperationQueue: MockOperationQueue!
    
    override func setUp() {
        super.setUp()
        // GIVEN
        mockOperationQueue = MockOperationQueue()
        sut = ImageFetcher(operationQueue: mockOperationQueue)
    }
    
    override func tearDown() {
        mockOperationQueue = nil
        sut = nil
        super.tearDown()
    }
    
    func test_cancelFetchingAllImages_calls_cancelAllOperations() {
        // WHEN
        sut.cancelFetchingAllImages()

        // THEN
        XCTAssertEqual(mockOperationQueue.cancelAllOperationsCountCallCount, 1)
    }
}

4a. Assert that calling a method in the SUT has a side effect in the SUT

Note: The code in this example is for illustrative purposes as some implementations are missing.
A very common pattern in iOS is to have a model object such as the previously defined Person and use it setup a custom view. For example:
struct Model { /* some properties */ }

class View: UIView {
    func configure(with model: Model) { /* configure the view */ }
}
This kind of example is best tested using screenshot testing instead of asserting each and every property that would be changed in the View by the Model. The test is simpler to write and if the View's implementation changed the differences could be seen clearly by inspecting the before and after screenshots of the view. This can be done using FBSnapshotTestCase, for example one test case for a Model with predefined properties:
class ViewTests: FBSnapshotTestCase {
    
    var sut: View!
    
    override func setUp() {
        super.setUp()
        sut = View()
    }
    
    override func tearDown() {
        sut = nil
        super.tearDown()
    }
    
    func test_ViewWithModel() {
        // GIVEN
        let model = Model( /* initialisation with mocked parameter*/ )
        
        // WHEN
        sut.configure(with: model)
        
        // THEN
        FBSnapshotVerifyView(sut)
    }
}

4b. Assert that calling a method in the SUT has a side effect in a mock.

Assuming we have a view controller with a table view displaying a list of strings. When a cell is tapped the view controller notifies it's delegate about this and passes the item selected. The code looks something like this:
protocol SelectionViewControllerDelegate: class {
    func didSelect(_ selectionViewController: SelectionViewController, item: String)
}

class SelectionViewController: UIViewController, UITableViewDelegate {
    
    private weak var delegate: SelectionViewControllerDelegate?
    private let items: [String]
    
    init(items: [String], delegate: SelectionViewControllerDelegate) {
        self.items = items
        self.delegate = delegate
        super.init(nibName: nil, bundle: nil)
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let item = items[indexPath.row]
        delegate?.didSelect(self, item: item)
    }
    
    required init?(coder aDecoder: NSCoder) { fatalError() }
}
To test the delegate pattern communication, the SelectionViewControllerDelegate can be mocked by creating an object that conforms to it and storing the item. The MockSelectionViewControllerDelegate can be injected using constructor injection. The capturedItem can then be used in a test case to assert that selecting a row in the tableView calls the delegate method with the correct item:
class MockSelectionViewControllerDelegate: SelectionViewControllerDelegate  {
    var capturedItem: String?
    func didSelect(_ selectionViewController: SelectionViewController, item: String) {
        capturedItem = item
    }
}

class SelectionViewControllerTests: XCTestCase {
    
    var sut: SelectionViewController!
    var mockDelegate: MockSelectionViewControllerDelegate!
    
    override func setUp() {
        super.setUp()
        mockDelegate =  MockSelectionViewControllerDelegate()
    }
    
    override func tearDown() {
        mockDelegate = nil
        sut = nil
        super.tearDown()
    }
    
    func test_tableViewDidSelectRowtAtIndexPath_calls_delegateWithSelectedItem() {
        // GIVEN
        let mockItems = ["a", "b", "c"]
        sut = SelectionViewController(items: mockItems, delegate: mockDelegate)

        // WHEN
        sut.tableView(UITableView(), didSelectRowAt: IndexPath(row: 1, section: 0))
        
        // THEN
        XCTAssertEqual(mockDelegate.capturedItem, "b")
    }
}

5. Assert that a change in a (mocked) dependency has a side effect in the SUT

An object used to fetch data from a URL that used the shared URLSession would look something like this:
enum Result<T> {
    case success(T)
    case failure(Error?)
}

class HTTPClient_Untestable {
    func fetchData(forURL url: URL, completion: @escaping (Result<Data>) -> Void) {
        // Use URLSession.shared to make a network request
    }
}
This object is not testable because it uses an internal dependency that cannot be accessed. Therefore, to extract the URLSesssion out we first declare a protocol that URLSession can conform to:
protocol URLSessionType {
    func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask
}
extension URLSession: URLSessionType {}
The updated HTTPClient now takes in a URLSessionType in the initialiser and the fetchData method uses this injected session object.
class HTTPClient {
    
    private let session: URLSessionType
    init(session: URLSessionType = URLSession.shared) {
        self.session = session
    }
    
    func fetchData(forURL url: URL, completion: @escaping (Result<Data>) -> Void) {
        let request = URLRequest(url: url)
        let task = session.dataTask(with: request) { (data, _, error) in
            guard let data = data else {
                completion(Result.failure(error))
                return
            }
            completion(Result.success(data))
        }
        task.resume()
    }
}
To test this, we mock the URLSessionDataTaskbecause it's an abstract class so resume needs to be overridden otherwise an exception would be thrown. Then we create a mock that conforms to URLSessionType. This mock stores the completion handler sent from the SUT.
class MockURLSessionDataTask: URLSessionDataTask {
    override func resume() {}
}

class MockURLSession: URLSessionType {
    var capturedCompletion: ((Data?, URLResponse?, Error?) -> Void)?
    func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
        capturedCompletion = completionHandler
        return MockURLSessionDataTask()
    }
}
This last test case uses XCTestExpectationbecause the fetchData function calls a completion handler asynchronously. Unfortunately, this means that the THEN part of the test partly needs to be defined before the WHEN. Note that we call the stored completion handler on the WHEN part to simulate a successful response from the session.
class HTTPClientTests: XCTestCase {
    
    var mockURLSession: MockURLSession!
    var sut: HTTPClient!
    
    override func setUp() {
        super.setUp()
        mockURLSession = MockURLSession()
        sut = HTTPClient(session: mockURLSession)
    }
    
    override func tearDown() {
        mockURLSession = nil
        sut = nil
        super.tearDown()
    }
    
    func test_fetchData_calls_completionWithSuccessResult_whenDataIsReturnedFromSession() {
        // GIVEN
        let mockURL = URL(string: "www.test.com")!
        let expectation = self.expectation(description: #function)
        sut.fetchData(forURL: mockURL) { result in
            // THEN (Partly defined before WHEN because of asynchronous nature of test)
            switch result {
            case .success(let data):
                XCTAssertEqual(data, Data())
            case .failure(let error):
                XCTFail("Unexpected failure with error: \(error)")
            }
            expectation.fulfill()
        }
        
        // WHEN
        mockURLSession.capturedCompletion?(Data(), nil, nil)
        
        // THEN (continued)
        waitForExpectations(timeout: 1, handler: nil)
    }
}
I’d like to thank Nahuel Marisi and Neil Horton for reviewing this article.

2 comments:

  1. Hello,
    The Article on Common unit testing techniques on iOS is nice give detail information about it. Thanks for Sharing the information about unit testing techniques. Software Testing Services

    ReplyDelete
  2. Good and Interesting article... thanks for sharing your information and valuable time. keep rocks and updating.

    Software Testing Training in chennai | Software Testing Training institute in chennai

    ReplyDelete