Tutorials‎ > ‎

Unit Testing in iOS

Introduction

Unit testing is a standard technique in computer programming whereby we break the software into small, independent pieces of code (a.k.a. the unit), isolate it from the rest of the code and test it. The main goal of unit testing is to try and find as many bugs as possible, as soon as possible.

Unit testing will be a large part of your work in the Software Development Project (and of your software engineer career!). Neglecting it will nearly always be a huge mistake!

We distinguish to kinds of testing :

  • Logit Unit Testing
    Logic unit testing concerns the testing of the classes logic directly. You only test the logic of the methods, you don't interact with user interface elements or simulate them. For example, testing that a coordinate convertor utility is returning correct values is logic unit testing.

  • Application Unit Testing
    Application testing in iOS is much more specific to applications with user interface. In application testing, you will check that clicking a certain button triggers the correct behavior, that a view loads correctly, and so on.

In this tutorial, you will learn how to logic test and application test your iOS applications in Xcode using the integrated tools.

A full tutorial on Unit testing can be found on Apple website :

Unit Testing in iOS

Requirements

For this tutorial, we suppose that you have already followed the two previous tutorials on iOS development : introduction and iOS in depth.
On the technical side, you will need to have Xcode 4.2 with iOS 5 installed. If you don't, ask the assistants for the installation packages.

Important note : you must have run the application once in the simulator before running any test (it needs the binaries).

Example of Logic Unit Testing

In this first example, we will learn how to create a Unit test. We will take the MyLocation app from previous tutorial, but with a new class Calculator added. This class is only composed of logic (inherits from NSObject) and is only there for illustrating this example.

We are going to create a logic unit test for the Calculator class (unit).

First, download the new version of MyLocation.
Open the MyLocation project (from the Download folder, on the right in the Dock)
Select File -> New Target


Select Other -> "Cocoa Touch Unit Testing Bundle" :


Enter the following values



You should now have a new Class called "CalculatorLogicTests". This class is in a new group called "CalculatorLogicTests". This group (called target) is separated from the MyLocation application and if we want to access classes of MyLocation from this new group, we need to do something.

Build Settings

We need to set the Build Settings of this testing target to use the classes of MyLocation target. To do so, go to MyLocation, select MyLocationLogicTests and go to Build Settings :


Select LogicTests or ApplicationTests depending of the part of the tutorial

Then, select All and Combined, search for "bundle loader" and enter the following exactly :

$(BUILT_PRODUCTS_DIR)/MyLocation.app/MyLocation

Search now for "Test Host" and enter the following information :

$(BUNDLE_LOADER)




END OF Build Settings step

We can now implement the tests. We create one method for each functionality. In our case, one for each possible operands of the calculator. Implement MyLocationLogicTests.m as following :

MyLocationLogicTests.m

#import "MyLocationLogicTests.h"

#import "Calculator.h"


@implementation MyLocationLogicTests


- (void)setUp

{

    [super setUp];

    

    // Set-up code here.

}


- (void)tearDown

{

    // Tear-down code here.

    

    [super tearDown];

}


- (void)testAddition {

    NSString* test = [Calculator computeWithOperand:@"+" number1:@"2" andNumber2:@"3"];

    NSString* realResult = [NSString stringWithFormat:@"%lf", 5.0];

    STAssertTrue([test isEqualToString:realResult], @"");

}


- (void)testSubstraction {

    NSString* test = [Calculator computeWithOperand:@"-" number1:@"2" andNumber2:@"3"];

    NSString* realResult = [NSString stringWithFormat:@"%lf", -1.0];

    STAssertTrue([test isEqualToString:realResult], test);

}


- (void)testProduct {

    NSString* test = [Calculator computeWithOperand:@"*" number1:@"2" andNumber2:@"3"];

    NSString* realResult = [NSString stringWithFormat:@"%lf", 6.0];

    STAssertTrue([test isEqualToString:realResult], @"");

}


- (void)testDivision {

    NSString* test = [Calculator computeWithOperand:@"/" number1:@"6" andNumber2:@"3"];

    NSString* realResult = [NSString stringWithFormat:@"%lf", 2.0];

    STAssertTrue([test isEqualToString:realResult], @"");

   

    STAssertThrows([Calculator computeWithOperand:@"/" number1:@"6" andNumber2:@"0"], @"");

    

}


- (void)testBadInputs {

    // must throw an exception to pass the test

    STAssertThrows([Calculator computeWithOperand:@"/+" number1:@"6" andNumber2:@"0"], @"");

    STAssertThrows([Calculator computeWithOperand:@"" number1:@"6" andNumber2:@"0"], @"");

    STAssertThrows([Calculator computeWithOperand:@"a" number1:@"6" andNumber2:@"0"], @"");

    STAssertThrows([Calculator computeWithOperand:@"ab" number1:@"6" andNumber2:@"0"], @"");

    STAssertThrows([Calculator computeWithOperand:@"+" number1:@"a" andNumber2:@"0"], @"");

    STAssertThrows([Calculator computeWithOperand:@"+" number1:@"5" andNumber2:@"a"], @"");

}


@end



Note : do not forget to import Calculator.h

We first see the two standard methods setUp and tearDown in which you can instantiate (reps. release) test instance variables. In our case, we test a class that only proposes a class method so we don't use these two utility methods.

Then each following method is a separate test for each functionality of Calculator class we want to test. We will see in the next section that separating tests is useful to be able to run them separately.

Finally, we can see that tests are called using the SenTest (ST) framework. A list of all possible tests is listed here.

We are now going to activate these tests. Select Product -> Edit Schemes...


Select the right scheme (MyLocationLogicTests) in the list, then select "Test" in the left column :



You can select here the tests you want to activate


We are now ready to run the tests. Click OK and select Product -> Test (cmd+U)


10) All the tests should and you should see the following outputs in the console :

Test Suite '/Users/<gaspar>/Library/Developer/Xcode/DerivedData/MyLocation-aigvggguqsokvbbpxwtxqdyqmhzp/Build/Products/Debug-iphonesimulator/MyLocationLogicTests.octest(Tests)' started at 2011-10-30 19:42:28 +0000

Test Suite 'MyLocationLogicTests' started at 2011-10-30 19:42:28 +0000

Test Case '-[MyLocationLogicTests testAddition]' started.

Test Case '-[MyLocationLogicTests testAddition]' passed (0.000 seconds).

Test Case '-[MyLocationLogicTests testBadInputs]' started.

Test Case '-[MyLocationLogicTests testBadInputs]' passed (0.000 seconds).

Test Case '-[MyLocationLogicTests testDivision]' started.

Test Case '-[MyLocationLogicTests testDivision]' passed (0.000 seconds).

Test Case '-[MyLocationLogicTests testProduct]' started.

Test Case '-[MyLocationLogicTests testProduct]' passed (0.000 seconds).

Test Case '-[MyLocationLogicTests testSubstraction]' started.

Test Case '-[MyLocationLogicTests testSubstraction]' passed (0.000 seconds).

Test Suite 'MyLocationLogicTests' finished at 2011-10-30 19:42:28 +0000.

Executed 5 tests, with 0 failures (0 unexpected) in 0.001 (0.001) seconds

Test Suite '/Users/<gaspar>/Library/Developer/Xcode/DerivedData/MyLocation-aigvggguqsokvbbpxwtxqdyqmhzp/Build/Products/Debug-iphonesimulator/MyLocationLogicTests.octest(Tests)' finished at 2011-10-30 19:42:28 +0000.

Executed 5 tests, with 0 failures (0 unexpected) in 0.001 (0.002) seconds



Example of Application Unit Testing

In this part, we will move to application unit testing. We will concretely interact with the user interface to simulate user inputs and test wether the application behave as it should.

Create a new Target (File -> New Target)
Select Other -> "Cocoa Touch Unit Testing Bundle"
Enter the following information :


--- Repeat now the part Build Settings (above) for this newly created target. ---

Once the Build Settings step done, we are going to implement the test case. Select MyLocationApplicationTests.h and edit it with the following content :

MyLocationApplicationTests.h

#import <SenTestingKit/SenTestingKit.h>

#import "MyLocationAppDelegate.h"

#import "NameViewController.h"

#import "MapViewController.h"


@interface MyLocationApplicationTests : SenTestCase {

    MyLocationAppDelegate* appDelegate;

    UINavigationController* mainNavigationController;

    NameViewController* nameViewController;

    MapViewController* mapViewController;

}


@end



You can see we need a pointer to the application delegate class itself. We will indeed check that the application starts correctly.


Now, implement MyLocationApplicationTests.m with the following :


MyLocationApplicationTests.m


#import "MyLocationApplicationTests.h"


@implementation MyLocationApplicationTests


- (void)setUp {

    [super setUp];

    

    // Set-up code here.

    appDelegate = [[[UIApplication sharedApplication] delegate] retain];

    mainNavigationController = (UINavigationController*)appDelegate.window.rootViewController;

    nameViewController = (NameViewController*)mainNavigationController.visibleViewController;

}


- (void)tearDown {

    // Tear-down code here.

    [appDelegate release];

    

    [super tearDown];

}


- (void)testApplicationDelegate {

    STAssertTrue([appDelegate isMemberOfClass:[MyLocationAppDelegate class]], @"bad UIApplication delegate");

    STAssertTrue([mainNavigationController isMemberOfClass:[UINavigationController class]], @"bad mainViewController");

}


- (void)testNameViewController {

    STAssertTrue([nameViewController isMemberOfClass:[NameViewController class]], @"");

    UIButton* nextButton = (UIButton*)[nameViewController.view viewWithTag:1];

    [nextButton sendActionsForControlEvents:(UIControlEventTouchUpInside)];

    STAssertTrue(mainNavigationController.visibleViewController == nameViewController, @"empty name check did not work"); //should not go to next view as username textfield is empty

    

    UITextField* nameTextField = (UITextField*)[nameViewController.view viewWithTag:2];

    nameTextField.text = @"Toto";

    [nextButton sendActionsForControlEvents:(UIControlEventTouchUpInside)];

    STAssertTrue([mainNavigationController.visibleViewController isMemberOfClass:[MapViewController class]], @"mapViewController not coming when touching next button");

    

}


@end



Explanations :
Wer have here two tests cases. The methods setUp and tearDown are called before and after each test case respectively, so that we can test each case separately.
In the method testApplicationDelegate, we test that the application delegate has started correctly by testing its class. We also test that the rootViewController of the window is a UINavigationController (as stetted in the code).

In the method testNameViewController, we test that possible user actions are handled correctly. We first simulate a click on the button next when the name text field is empty. It should not go to the map. We then fill the name text field and click on the next button again. Now, the map should come, and we test it by testing the visible view controller.

These test cases are far from being complete. Many other test should be done to cover all possible user actions and running paths. we have nevertheless let them aside here for clarity purposes.

You may have noticed that to find the user controls (button, text field), we use the tag property of the views. So that these tags are correct, we need to set them for the the button and the text field accordingly.

To do so, go to NameView.xib, select the button and change its tag to 1 in the right column :


Do the same for the text field with tag = 2 :



Now, go to Product -> Edit Schemes and activate the tests for the MyLocationApplicationTests target :



Finally, run the application test ! (Product -> Test).

You should have an output that looks like this :

Test Suite 'All tests' started at 2011-11-13 21:32:12 +0000

Test Suite '/Users/<gaspar>/Library/Developer/Xcode/DerivedData/MyLocation-glwkfxnrtujdosesedhjfujyjnxy/Build/Products/Debug-iphonesimulator/MyLocationApplicationTests.octest(Tests)' started at 2011-11-13 21:32:12 +0000

Test Suite 'MyLocationApplicationTests' started at 2011-11-13 21:32:12 +0000

Test Case '-[MyLocationApplicationTests testApplicationDelegate]' started.

Test Case '-[MyLocationApplicationTests testApplicationDelegate]' passed (0.000 seconds).

Test Case '-[MyLocationApplicationTests testNameViewController]' started.

Test Case '-[MyLocationApplicationTests testNameViewController]' passed (0.150 seconds).

Test Suite 'MyLocationApplicationTests' finished at 2011-11-13 21:32:13 +0000.

Executed 2 tests, with 0 failures (0 unexpected) in 0.150 (0.150) seconds

Test Suite '/Users/<gaspar>/Library/Developer/Xcode/DerivedData/MyLocation-glwkfxnrtujdosesedhjfujyjnxy/Build/Products/Debug-iphonesimulator/MyLocationApplicationTests.octest(Tests)' finished at 2011-11-13 21:32:13 +0000.

Executed 2 tests, with 0 failures (0 unexpected) in 0.150 (0.150) seconds

Test Suite 'All tests' finished at 2011-11-13 21:32:13 +0000.

Executed 2 tests, with 0 failures (0 unexpected) in 0.150 (0.151) seconds


Comments