This post will explain how (and why) to get started unit testing in iOS.
Table of Contents:
- Why I added unit test
- Add Unit test to your project
- Getting started with unit test
- Unit test example
Does this sound familiar to you?
You adjusted a function and want to check if the function produced the correct output, you build and run the app, open the simulator, input username and password, tap login, and then tap another 3 buttons to reach the screen that contains the label text generated by the function. You then found the output is incorrect, and then you adjust the function and....
I have been there and spent countless time tapping into the correct screen to check output over and over again. I remember hearing some iOS developers mention about unit test in Reddit and local meetup but I didn't get why I should write unit test at that time, until recently an user emailed me about a bug in my app and I decided to take a plunge into unit testing.
The app in question is Rapidly, it is an app to check route and fare information for public trains in Kuala Lumpur.
A user emailed me to notify that the "take the train moving towards X end station" text would display wrong result when a certain combination of start and end stations are chosen. In Kuala Lumpur train system, different lines of train can share the same rail in some station but they will end up in different terminal station, it can be confusing for new visitor as the trains arriving the same platform can lead to different destination stations.
This is the text in question:
After receiving the email, I proceed to check the related function and make adjustment to it. To check the text output, I have to repeat this process:
Each time I made change to the function, I have to repeatedly select two stations, tap 'Show Route' and scroll down to check if the text shown is correct or not. I found this process quite tedious and time consuming as I have to spent 15 seconds+ (including build time) just to check a line of text.
Eventually I got fed up and decided I should add unit test to my app project. I will briefly show how to add unit test to a new / existing project below.
1 - How to add unit test target to project
New project
During creation of new project, you can include unit test by checking the box Include Unit Tests
.
Existing project
For existing project, select File > New > Target. Then select 'iOS Unit Testing Bundle' and click 'Next'.
You will see a window like above, Xcode will autofill the Product Name field with {Your app name}Tests
, usually I left this field as is and click 'Finish', you can change the product name if you want.
Test boilerplate
After creating the test target, you can view the test boilerplate which Xcode has created for you by selecting {AppName}Tests folder > {AppName}Tests.swift in navigation bar.
We will explain how to start writing unit test in the next section.
2 - Getting started with Unit Test
Let's kickstart with a simple test, we will add a check like this inside func testExample()
:
func testExample() {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
let a = 3
let b = 2
XCTAssert(a > b, "a should be larger than b")
}
Press command + U to run the test.
After instructing Xcode to run the test, you will see Xcode proceed to build the app and run the test immediately. You should see the message 'Test Succeeded' after the test has finished running.
Congratulations! You've just ran the first test!🎉
What Xcode does when you press command + U is that it will go over all functions which have name starting with 'test' (eg: testExample, testPerformanceExample etc) and run the code inside the function.
XCT
in XCTAssert
is a shortform for X code Test, it is a utility function to check if an output of a function / syntax equals to true.
XCTAssert(a > b, "a should be larger than b")
will check if a > b
return true. If a > b
return false, the test will fail and Xcode will show the error message a should be larger than b
like this:
You can try out different Assert function as suggested in autocomplete:
XCTAssertNil
will check if the expression return nil and show an error message if it is not nil, XCTAssertTrue
will check if the expression return true and show an error message if it is not true, etc.
One thing to keep in mind is that if a function name doesn't start with 'test', it will not get executed in test!
Pressing Command + U will run all the tests and this will take quite some time, if you just want to run a specific test function (to save time during development), you can click the diamond shaped sign (whether its a green tick, red cross or empty) beside the function name :
In the next section, I will show how I added test for the 'Towards X' text function for my app Rapidly.
3 - Unit Test example
In this section, I will show how I added test for the 'Towards X' text function for my app Rapidly.
The function that generates the 'Towards X' string is inside a class named 'RapidHelper.h' (Yes, the app is unapologetically using Objective-C, fite me 😂)
Since this function is located inside the class 'RapidHelper' , the convention I follow is to create a unit test case file named 'RapidHelperTests' ( {ClassName}Tests ), like this:
Right click on the 'RapidlyTests' group, select 'New File' > 'Unit Test Case Class'.
In the RapidHelperTests.m
file, I start by importing classes that will be used by the test cases. (You won't need to do this if you are using Swift)
These are the stations in question (wrong output when this particular start station and end station is used):
Since the start station and end station are named Plaza Rakyat and Chan Sow Lin, I named the test function as testTowardsStringFromPlazaRakyatToChanSowLin. I can use the formula testTowardsStringFrom{StartStation}To{EndStation}
for naming should I need to test other station combinations in the future.
To ensure the output of the function towardsStringFromNode:toNode
is correct for the station Plaza Rakyat
and Chan Sow Lin
, here's the step I wrote for the test function:
- Initialize station
Plaza Rakyat
andChan Sow Lin
- Call the function
towardsStringFromNode:toNode
by passing the above stations to its parameters, then store the result into a variable - Compare the variable with the expected output
- (void)testTowardsStringFromPlazaRakyatToChanSowLin {
/**
Initialize Plaza Rakyat station (of AmpangSriPetaling Line)
*/
StationNode *SP8 = [StationNode nodeWithIdentifier:@"SP8"];
SP8.additionalData = [NSMutableDictionary dictionaryWithDictionary:@{@"name": @"Plaza Rakyat", @"line" : [NSNumber numberWithInteger:LineAmpangSriPetaling]}];
/**
Initialize Chan Sow Lin station (of AmpangSriPetaling Line)
*/
StationNode *AG1SP11 = [StationNode nodeWithIdentifier:@"AG1"];
AG1SP11.additionalData = [NSMutableDictionary dictionaryWithDictionary:@{@"name": @"Chan Sow Lin", @"line" : [NSNumber numberWithInteger:LineAmpangSriPetaling]}];
// Run the test and check if the string produced by the function is equal to the desired output
NSString *towardsString = [RapidHelper towardsStringFromNode:SP8 toNode:AG1SP11];
XCTAssertTrue([towardsString isEqualToString:@"Ampang / Putra Heights"], @"Towards String from Plaza Rakyat station to Chan Sow Lin station should be 'Ampang / Putra Heights'");
}
Now that I have the test set up, every time I made changes to towardsStringFromNode:toNode
function, I just need to run this individual test case to check if the output is correct instead of having to wait the simulator to load and then spent 10 seconds tapping around 😅.
Afterthought
Although this post doesn't go deep into unit test, I hope it does answer the question "why should I write unit test?" for you. It saved me a lot of time and effort on developing apps, and I hope you can benefit from it too. Next time if you find yourself tapping a lot of buttons or navigating multiple screens to check if the output is correct, it might be a good time to write unit test for that particular function.
If you are still not convinced by the benefits of unit testing, here is the full list of benefits of unit testing written by Josh Brown.