Unit Testing in iOS

本文将指导您如何使用Xcode进行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

anchor.gifRequirements

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).

anchor.gifExample 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



30170001_FAoQ.png

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

30170001_bHf4.png

Enter the following values

30170002_ABtm.png


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.

anchor.gifBuild 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 :

30170003_a6zz.png

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
30170004_2peO.png

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

$(BUNDLE_LOADER)
30170004_DcuG.png

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...

30170005_x9R6.png

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

30170006_ENuW.png


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)

30170006_Jd1i.png

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 :

30170007_wZyu.png

--- 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 :

30170007_wKjk.png

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

30170008_FE5Y.png


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

30170009_FgvW.png


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






转载于:https://my.oschina.net/u/557242/blog/106459

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值