Unit Testing in iOS

Ha Duyen Hoa
9 min readDec 2, 2018

--

Part 4: How to deal with contexts.

Preface

I’m planning to write a complete series of Unit Testing for iOS. You can find finished parts in the links below. Happy reading :)

Part 1: Why do we need Unit Testing when developing an application?

Part 2: My first Unit Tests

Part 3: Quick/Nimble for a clean test

Part 4: How to deal with contexts

Part 5: Good practices

Part 6: Unit Test, Jenkins & SonarQube altogether [TO BE WRITTEN]

As per definition, Unit testing is to test a unit of work. Unit of work is presented as a function implemented in the application. In order to have high code coverage, we have to test as much as possible functions that are declared & implemented.

Functions are not simple. They do not just calculate a sum of two variables in the example of my second writing. Most functions are complex and often depend on the context when it’s running. For example, the behavior of the function getServerConfigData in my writing depends on network condition, on the server’s state, on the server’s configuration, etc. Or when we test a Translator class that uses NSLocalizedString API to retrieve the translation, the retrieved text can be different when we launch the test case in two simulators, one set to French and one set to English. Another situation is when we need to test functions that read & write data from DB via CoreData. The DB content can be changed when we run multiple tests one after another. This may affect on test result as the input of the test is not as expected anymore.

I call all of them “context” in Unit Testing. And the behavior of those things are often unpredictable and therefore make our tests reliable. The purpose of this writing is to help you identify the context and how to deal with it to make your unit tests independent, reliable and robust.

Which contexts do we face?

We can categorize them in 3 main types, by the way of how to deal with them when writing unit tests.

  • Network
  • External libraries
  • Local resources (files or databases)

Network context is the dependency of your unit test to the network connection and to the behavior of the back-end side. In fact, when we test, we should avoid this dependency. In other words, we must find a way to set up the beforeTest (or setUp) step so all network call should return the expected result as per required by the test case.

External libraries context is the dependency of your unit test to the implementation of the libraries that you use in your function to be tested. Here, I don’t want to say that we need to test external libraries, but to test the function that uses them. As we cannot change the implementation of those codes, we won’t be able to make them behave follow the scenario of your test case. If we don’t know how to deal with it, we won’t be able to test our functions either.

Local resources context is the dependency of your unit test to the local resources that the test app needs to use during testing. Each function handles the local resource in its own way. At some points, the resources are mixed up and not “ready” to test if they are not cleaned up before each test.

Stub network calls

This is one of the solutions to make our test executes independently with network calls. Network stubbing is a way to force HttpRequest to respond with the desired data/error. There are several libraries that can help you to achieve that. In my projects, I used OHHTTPStubs.

The process to stub is straightforward: call OHHTTPStubs.stubRequests(...) function each time you want to stub a request. You can choose to stub all requests that can be called during the test, or just to stub some of them. My recommendation is to stub all request, and choose the right OHTTPStubsResponse object for each of those requests.

OHHTTPStubs.stubRequests(passingTest: { request -> Bool in
//return true if we want to stub that request
return true
}, withStubResponse: { request -> OHHTTPStubsResponse in
//return the desired response (or error)
return OHHTTPStubsResponse(data: "ok".data(using: .utf8)!, statusCode: 200, headers: nil)
})

You can find many examples for different situations in the GitHub of that library.

Basically, the code above is called before the test function, means in overridden setUp function in XCTest or beforeEach block in Quick test in most of the time.

An example in Quick for the class NetworkProviderTests in the sample project of my writing.

To keep code in test class clean, we should have some TestHelper that give return the required responses (text) from the test bundle. The code above will be something like that:

OHHTTPStubs.stubRequests(passingTest: { request -> Bool in
//return true if we want to stub that request
return true
}, withStubResponse: { request -> OHHTTPStubsResponse in
//return the desired response (or error)
return OHHTTPStubsResponse(data: TestResource.testData(filename: "network.config.get.ok", extension: ".json"), statusCode: 200, headers: nil)
})

Subclass

By designing the architecture of our iOS app with more Dependency Injection (DI) pattern than Singleton pattern, we can use the Subclass technique to break the dependency of your test classes to the test context.

To facilitate the use of subclass technique, there are 2 conditions:

  • The class and related methods of that class must be subclassable. In Swift 3.x - 4.x, we declare class as public or open so it can be subclassed. public allows subclassing from the same module, and open allows subclassing from everywhere.
  • Subclassing class can be used in place of the original class where it’s used. This is possible when we design the application with Dependency Injection pattern. If we use Singleton pattern, subclassing will not help. In that case, we will need to use Mockup technique (section below).

Subclassing in action.

Come back with the class NetworkProvider.swift. The goal is to use a fake URLSession to manipulate the response of that object in order to simulate different cases as expected.

By looking at the current code, we find out that the function getServerConfigData(_ completion: ...) uses the singleton URLSession.shared to create a download task to get the data from our server. The way of writing this function is not friendly for subclassing (and for unit testing in general). We should change it first with DI pattern, by adding the parameter of type URLSession to that method:

/**
DI version of the function 'getServerConfigData' above
- Parameter urlSession: the URLSession's instance that is used to handle network connections.
- Parameter completion: completion block to be called when the download task is done.
- Parameter downloadedData: Downloaded data or nil
- Parameter error: an error if occurs or nil
*/
class func getServerConfigData(_ urlSession: URLSession,
completion: @escaping (_ downloadedData: Data?, _ error: Error?) -> Void) {
guard let url = URL(string: "https://google.com") else {
completion(nil, NSError(domain: "dummytest.ch", code: 999, userInfo: nil))
return
}

//create download task and start to download
let dataTask = urlSession.dataTask(with: url) { (data, _, error) in
completion(data, error)
}

dataTask.resume()
}

With the new implementation, we can easily inject any subclass of URLSession to break the dependency with real network connections.

What we have to do now is to create a subclass of the class URLSession and change the behavior of the belated function that we use above. Take a look at the dataTask(...) function above, we see that it returns an URLSessionDataTask that at some point calls the completion with 3 objects (or nil): Data?, URLResponse?, Error? So, I create my fake class like this.

class FakeUrlSession: URLSession {
private var responseData: Data?
private var error: Error?
private var urlResponse: URLResponse?

override func dataTask(with url: URL,
completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
return FakeUrlSessionDataTask({
completionHandler(self.responseData, self.urlResponse, self.error)
})
}
}

Here, I used also another fake class FakeUrlSessionDataTask that also change the behavior of the URLSessionDataTask by just calling the completionHandler when it’s asked to do a resume()

Once you have created FakeURLSession , you can use it to create as many setUp scenarios as you want. Here, I improved a little bit this class to facilitate using it. The final version is:

Now, you don’t have to stub the network call anymore and you are still able to resolve the network context in your tests. Here is the test code using Quick/Nimble:

//MARK: test with subclassing
context("using my DI NetworkProvider") {
it("should be able to get server config correctly when server is up and running") {
var downloadedData: Data?
var networkError: Error?
let fakeUrlSession = FakeUrlSession(.success(data: "ok".data(using: .utf8)!))

NetworkProvider.getServerConfigData(fakeUrlSession, completion: { (data, error) in
downloadedData = data
networkError = error
})

expect(downloadedData).toNotEventually(beNil())
expect(String(data: downloadedData!, encoding: .utf8)) == "ok"
expect(networkError).to(beNil())
}

it("should receive error when server is down") {
var downloadedData: Data?
var networkError: Error?

let fakeError = NSError(domain: "dummytest.ch", code: 500, userInfo: nil)
let fakeUrlSession = FakeUrlSession(.failure(error: fakeError))

NetworkProvider.getServerConfigData(fakeUrlSession, completion: { (data, error) in
downloadedData = data
networkError = error
})

expect(downloadedData).toEventually(beNil())
expect(networkError).toNot(beNil())
expect((networkError as NSError?)?.code) == 500

}
}

Mocking

Subclassing is one of Mocking technique which is done by creating a subclass and modify the default behavior of the parent class.

Another possibility to do Mocking is to design your modules/classes with protocols. Then, in the Test target, you are free to create your own class that conformed to that right protocol and with your own implementations of that protocol.

We can also use swizzling method to change the behavior of a class by replacing the original method by our fake method. I don’t recommend to use it because it can be overkill to manage.

Using local resources during tests

Network and external libraries are not the only things we need to isolate during executing tests. Local resources are too. When the test suite is running, its test cases may create repeatedly many different data into files or database. Hence, later tests may fail because data they are using may not as expected. To resolve it, it’s much easier than to deal with network context and external libraries context, technically. However, it still requires some works though.

In XCTest, we can do a cleanup of those data by using the setUp and tearDown() to make all test run with the required state. In Quick/Nimble, we do that by adding the cleaning code to before... block.

To keep your test classes clean and easy to read and maintain, you should organize the loading or cleaning of those resources in a (kind of) TestHelper class.

Instead of putting the fake data directly to the test class, we create resource files and use TestHelper to retrieve it.

Then, we need to create TestContext classes to handle the context to be used why testing. Those TestContext must be able to update the content of the context (either file or database). Those TestContext can be used not only for files or databases resources but also for any business context of the application during the integration test.

That’s it.

Having a good Unit Test requires as much effort as writing function. By writing clean Unit Test, you will improve also the skill to write a clean application. When creating test classes, we should try to make the test independent with any context that it belong to. You can choose one of the techniques above for your case.

You can find codes in examples of this post in my git repo: https://bitbucket.org/haduyenhoa/testingfordummies

Part 5 is ready. I was so busy with my move from Lausanne to Zug and I can only finish it recently ^^

--

--

Ha Duyen Hoa

a Software Engineer who loves audiophile music & traveling.