Chapter 20. Unit Testing

WHAT'S IN THIS CHAPTER?

  • Learning how unit tests work

  • Adding unit tests to your Objective-C project

  • Configuring unit tests for an iPhone app

  • Adding unit tests to your C or C++ project

Unit testing is a way of validating the run time behavior of your code at build time. In other words, you can test and verify the functionality of individual modules before you assemble them into a working application.

In addition to writing your application's classes, you also write one or more unit tests that exercise those classes and verify that they perform as expected. These tests are not part of your application and live in a separate unit test bundle. You can elect to have these tests performed on your code whenever you build your product, ensuring not only that your code compiles and links but that it behaves correctly as well.

Unit testing is a fundamental part of Test Driven Development (TDD), a development philosophy popularized by the Extreme Programming movement. In TDD, you develop the test for your function or class first, and then write your function to meet the expectations of the test. In essence, this is what rigorous designers have done for years. They first develop a detailed specification of exactly what a function should do, write the function, and then verify that the function behaves as designed. The quantum leap provided by unit tests is that the "specification" is now an automated test that verifies the design goals of the code rather than a paper description that must be interpreted by the programmer and verified by a quality assurance engineer. Because the test is automated, it can be run every time the application is built, ensuring that every function still conforms to its specification, or immediately alerting the developer if it does not.

Whether or not you subscribe to the principles of Extreme Programming, unit tests provide a powerful tool for avoiding issues like the so-called "forgotten assumption" bug. The typical scenario goes like this:

  1. You develop a complex application.

  2. You then decide to add a new feature to some core class.

  3. You make the change in what you think is a completely transparent manner, only to discover that some other part of your application now fails miserably.

This is invariably a result of one of two problems: either you inadvertently introduced a bug into the core class, or you forgot about an assumption made by the client code that uses the class. Unit tests can help avoid both of these pitfalls.

Xcode supports unit testing of C/C++ and Objective-C applications using two different technologies. Although the concepts and initial steps are the same, most of the details for creating and using unit tests differ for the two languages. After you get past the basics, skip to either the Objective-C or C++ section, as appropriate, for integrating unit tests into your application.

HOW UNIT TESTS WORK

Unit tests are little more than code — which you write — that exercises the classes and functions in your project. You are entirely responsible for determining what and how your code is tested. Your tests are compiled into a unit test bundle, which is produced by a unit test target added to your project. The collection of tests in a unit test target is called a test suite. To run the tests, all you do is build the unit test target. The target first compiles your tests. It then runs a special build script phase that loads your test code, runs all of the tests, and reports the results. If any of your tests fail, the build process reports these as errors and stops. Figure 10-1 shows the build log from a project with unit tests.

FIGURE 20-1

Figure 20-1. FIGURE 20-1

Unit test bundles are part of the build process. The code associated with unit testing is compiled into the unit test bundle and should never be included in your final product.

GETTING STARTED WITH UNIT TESTS

There are four basic steps to adding unit tests to a project:

  1. Create a unit test target.

  2. Configure the target and your application for unit tests.

  3. Write some tests.

  4. Integrate the tests into your development workflow.

How you approach each step depends on a number of decisions. The biggest decision is whether to create independent or dependent unit tests. Each has its own advantages and disadvantages. The one you choose will determine how you configure your unit test target, your application target, and how your tests can be integrated into your development workflow.

Note

Don't confuse dependent unit test with target dependencies. Although a dependent unit test target typically depends on its subject target, the term "dependent" has to do with the fact that unit test bundle is not self-contained. Both dependent and independent unit tests may depend on other targets, or not.

Independent Unit Tests

Independent unit tests are the simplest to create and use, but they have a couple of drawbacks. An independent unit test bundle includes both the tests and the code to be tested. All are compiled and linked into the unit test bundle. At build time, the bundle is loaded and all of the tests are executed.

  • Advantages:

    • Self-contained

    • No special code required

  • Disadvantages:

    • Doesn't test actual product

    • Code must be compiled twice

The advantage to independent unit tests, and where they get their name, is that the target and unit test bundle are entirely self-contained. All of the code to be tested is compiled by the target. That is, the target is independent of any other applications or products that your project produces. In fact, the code doesn't even need to be compiled elsewhere. You could, conceivably, create a project that only tests code and produces no products whatsoever.

The disadvantage is that the code being tested is compiled separately from the same code that gets compiled when your application is built. One consideration is the fact that the code could be compiled differently for the unit test bundle and the application. Build-setting differences between the unit test target and your application's target could easily cause subtle differences in the code the compiler produces, which means that your tests are not actually testing the same code that will run in your application. For most code, this probably won't matter, but a difference in, say, the signedness of character variables, optimization, or the size of enums could cause your tests to miss bugs in your application's code or fail tests that should pass. If you are rigorous, or just paranoid, you'll want to test the actual code that your final application will be executing — not just a reasonable facsimile.

The other, potential, disadvantage to recompiling all of the same code is that it takes time. All of the code you intend to test will have to be compiled twice — once for the application and again for the unit test bundle. If your code base is large, or it depends on a lot of other code that must be compiled, then compiling everything twice will slow down your builds.

Dependent Unit Tests

Dependent unit tests perform their tests on the actual code produced by your product. A dependent unit test bundle contains only the test code. When it comes time to perform your unit tests, the program or library that your project produced is loaded into memory along with the unit test bundle. The references in the unit test bundle are linked to the actual classes and functions in your application and then executed. The unit test bundle depends on another product to accomplish its purpose.

  • Advantages:

    • Tests actual code

    • Code only compiled once

  • Disadvantages:

    • Test environment may be awkward

    • Dependent on other targets

As you might guess, there's more than just a little sleight of hand involved here. The unit test framework uses two techniques, depending on what kind of product you're testing. The method used to test libraries, frameworks, and independent unit tests is pretty straightforward: the unit test target executes a testing program that loads the unit test bundle (containing the test code and possibly some code to be tested) along with any dynamic libraries or frameworks that need testing. The tests are executed and the testing utility exits.

Testing an application is decidedly more bizarre. The unit test target runs a script that launches the actual executable produced by your project. Before the executable is started, several special environment variables are configured. These settings are picked up by the system's dynamic library loader and cause it to alter the normal sequence of binding and framework loading that occurs at run time. The settings instruct the loader to first load a special unit test framework into the application's address space. This process is known as bundle injection. The testing framework causes your unit test bundle to also be loaded into memory. Initialization code in your unit test bundle intercepts the execution of your application, preventing it from running normally. Instead, the unit test bundle's code links directly to the functions defined in the application and executes all of the tests. It then forces the application to terminate.

However convoluted, the beauty of this process is that your unit tests will test the actual, binary code of your application; the same code that will run when your application launches normally. The disadvantage is that this process is complex and requires a number of concessions from your application. Mostly these are restrictions on how your application is built. In the case of some C/C++ applications, you are also required to add code to your application to support dependent unit testing.

iPhone Unit Tests

The iPhone SDK supports unit testing too. The techniques are very similar to the Objective-C unit testing under Mac OS X — in fact, they both use the same testing framework — but with the following differences:

  • Independent unit tests are called logic tests in iPhone parlance, and are executed using the iPhone simulator.

  • Dependent unit tests are called application tests in iPhone parlance, and are preformed on an actual iPhone or iPod Touch.

  • Setting up an application test suite for the iPhone is significantly different than setting up a dependent test suite for a Mac OS X application.

Except for those configuration differences, you can follow the guidelines and instructions for writing Objective-C unit tests when developing for the iPhone, substituting the terms "independent test" and "dependent test" with "logic test" and "application test."

Note

iPhone unit testing requires iPhone OS 3.0 or later.

ADDING A UNIT TEST TARGET

The first step in adding unit testing to a project is to create a unit test target. Choose Project

ADDING A UNIT TEST TARGET

Note

Some releases of the Xcode Development Tools, particularly those intended for iPhone development, do not include the older Carbon and C++ target templates, so your installation might not have a Carbon Unit Test Bundle template. You can "borrow" one from an older Xcode installation or try installing the Mac OS X Xcode package.

Give the target a name and select the project it will be added to. Choose a name that reflects the subject of the test. For example, if you were writing tests for a target named HelperTool, you might name the unit test target HelperToolTests.

FIGURE 20-2

Figure 20-2. FIGURE 20-2

Xcode creates a new unit test target and adds it to your project. You now need to configure it properly and populate it with tests. How you configure your unit test target depends on what kind of unit test it is and what kind of product it tests.

Note

You might be anxious to try out your new unit test target, but you can't until it is configured and you have added at least one test; a unit test bundle will fail if it doesn't contain any tests. The "Creating a Unit Test" section, later in this chapter, tells you how to add tests to your unit test bundle.

Unit Test Target Dependencies

Unit tests are part of the build process. Target dependencies are used to integrate unit tests into your build. What target dependencies you create (if any) will depend on the kind of unit test you are creating.

Independent Unit Test Dependencies

Because independent/logic unit tests are self-contained, they do not (technically) need to be dependent on any other targets. All of the code that needs to be tested will be compiled when the target is built. Whenever you want to run your unit tests, simply build your unit test target.

One of the main tenets of test driven development is that your unit tests should be performed automatically every time you build your project. To do that, follow these steps:

  1. Set the active target to your application target.

  2. Make your application target dependent on your unit test target.

Now every time you build your application, Xcode will first build and run all of the unit tests.

Alternatively, you could make the unit test target dependent on your application target; then you have the choice of just building your application or building your application and running all of your unit tests. You could also leave your application and unit test targets independent of each other and create an aggregate target that builds both. As you can see, independent unit test targets are pretty flexible.

Dependent Unit Test Dependencies

Dependent Mac OS X (but not iPhone) unit test targets must depend on the target, or targets, that produce the products they test. Otherwise, there is no guarantee that the tests will be performed on up-to-date code. If you want unit tests run every time you build your product, follow these steps:

  1. Set the active target to the unit test target.

  2. Set the active executable to the results of the product target.

Now every time you build, the application is built followed by a run of all of the unit tests. The build will only be successful if both the build and the unit tests pass muster.

Using this arrangement, you can easily ignore unit tests by building just the product target, or making another target dependent on the product target directly. In a project with many product and unit test targets you could, for example, create two aggregate targets: one that depends on all of the product targets for "quick" builds and a second that depends on all of their respective unit test targets for "full" builds.

An iPhone unit test target's dependencies are inverted from those used by dependent unit test targets. The section "Configuring an iPhone Application Test" shows you both how to configure the iPhone application unit test target and set up its dependencies.

Configuring an Independent/Logic Unit Test

Independent unit tests require no special configuration. All you need to do is make the source code for both the tests and the code to be tested members of the target. The compiler build settings for the target should match those of your product target as closely as possible, so that the code produced when you're compiling the unit test target is as close as possible to the code that will be compiled into your final product.

Add the source files to the target by dragging them into the Compile Sources phase of the unit test target, or by opening their Info window and adding them to the unit test target in the Targets tab. You can add the source for the actual tests in a similar manner (if the tests already exist), or by adding them to the unit test target when you create them. The section "Creating a Unit Test" shows how to write and add a new test.

The target SDK for an iPhone logic test (independent unit test) must be set to the iPhone Simulator.

Configuring a Mac OS X Dependent Unit Test

A dependent unit test needs to know where to load the application or libraries to be tested.

Testing an Application

For applications, you accomplish this by setting the Bundle Loader (BUNDLE_LOADER) and Test Host (TEST_HOST) build settings. These should both be set to the executable you want to test. Follow these steps to quickly set both values:

  1. In the Info window of the unit test target, select the Build tab. Choose All Configurations. Arrange the windows so that Groups & Files list in the project window and the Info window are both visible. Expand the Products group in the project source group.

  2. In the target's Info window, find the Bundle Loader setting — you'll find it in the Linking group — and click in its value field to edit it. In the Products smart group, locate the executable product to be tested and drag it into the value field of the Bundle Loader setting. Xcode inserts the full path to the executable. For application bundles, you need to locate the application's binary executable — the folder with the extension .app is not an executable. You can manually supply the path, or follow these steps:

    1. Right/Control+click the product and choose Reveal in Finder.

    2. In the Finder, Right/Control+click the application and choose Open Package Contents.

    3. Open the Contents folder.

    4. Open the MacOS folder.

    5. Drag the application's executable into the Bundle Loader setting's value cell in the target's Info window.

  3. Select the beginning of the path that represents the build location for the current build configuration. Typically this is /path/to/project-folder/build/build-configuration name/, but it may be different if you have altered the default build locations. Replace this portion of the path with the $(CONFIGURATION_BUILD_DIR) macro. In a project that produces a simple command-line executable, the final Test Host path will look like $(CONFIGURATION_BUILD_DIR)/ProgramName. For a bundled Cocoa or Carbon application, the Bundle Loader path will look something like $(CONFIGURATION_BUILD _DIR)/AppName .app/Contents/MacOS/AppName.

  4. Locate the Test Host setting — you'll find it in the Unit Testing group — and double-click its value field to edit it. Enter $(BUNDLE_LOADER) as its value. This sets the TEST_HOST build setting to the same value as the BUNDLE_LOADER setting.

The Bundle Loader setting tells the linker to treat the executable as is if were a dynamic library. This allows the tests in the unit test bundle to load and link to the classes and functions defined in your application.

The Test Host setting tells the unit test target's script phase the executable that will initiate testing. When testing an application, it is the application that gets loaded and launched. The injected testing framework and bundle intercepts the application's normal execution to perform the tests.

Preparing Your Application

A few concessions are required of applications being tested by dependent unit test bundles. You must make these changes in the target that produces your application, not the unit test target. These requirements do not apply to independent unit tests or when you're testing dynamic libraries or frameworks.

Open the Info window for the application target and choose the Build tab. Choose All Configurations and set the following:

  • Set ZeroLink to NO (uncheck the box).

  • If your project is a C++ program, find the Symbols Hidden By Default setting and turn it off (uncheck the box).

ZeroLink must be turned off for your application. The ZeroLink technology is incompatible with the techniques used to intercept the application at run time. ZeroLink has been deprecated in Xcode 3, so you may not even see it in your build settings, but projects from prior versions of Xcode may still have it set.

The Symbols Hidden By Default option must be disabled for C++ applications so that all of the classes and functions defined by your application appear as external symbols. The unit test target must link to the symbols in your application, so these symbols must all be public. Objective-C tests are all resolved at run time by introspection, so they don't require any public symbols at link time.

Testing Libraries and Frameworks

When you're constructing a unit test to test a dynamic library or framework, leave the Bundle Loader and Test Host settings empty. This is because the "program" to be loaded for testing will be the unit test bundle itself. If the Test Host setting is blank, the script launches the otest (for Objective-C) or CPlusTestRig (for C/C++) tool instead. The testing tool loads the unit test bundle and runs the tests it finds there, with the assumption that the unit test bundle either contains (in the case of independent tests) or will load (in the case of dependent tests for libraries and frameworks) the code to be tested.

For dependent unit tests that test libraries or frameworks, the unit test bundle is the client application. Configure your unit test bundle exactly as you would an application that uses those libraries or frameworks, adding the frameworks to the target and including whatever headers are appropriate to interface to them. The dynamic library loader takes care of resolving the references and loading the libraries at run time.

Configuring an iPhone Application Test

Testing an iPhone application is different from testing a Mac OS X application, and requires a different organization in Xcode. In Mac OS X development (described in the previous section), you tell the unit test bundle what product you want tested. It takes on the responsibility of loading that target, injecting itself into the application, and performing its tests.

In iPhone development, the roles of the application and unit test bundle are reversed. You create a custom version of your application that includes the unit test bundle product. You load and run your test app on your iPhone or iPod Touch device like any other app. Once started, your app loads the unit test bundle, which takes over and performs its tests.

To configure an iPhone app for unit testing, follow these steps:

  1. Add a Unit Test Bundle target, using the Cocoa Touch Unit Test Bundle template, as described in the beginning of this section. This is your unit test target.

  2. Duplicate the target that builds your app. Give it a descriptive name like MyAppTesting. This is your test app target.

  3. Make your test app target dependent on your unit test bundle target.

  4. Add the product of the unit test target (the MyTests.octest bundle) to the Copy Bundle Resources phase of your test app target. This will include the compiled suite of unit tests in your app's resource bundle.

  5. Set the active target to the test app target.

  6. Set the Target SDK to iPhone Device 3.0 or later.

  7. Build and run your test app target. The test results will appear in your console window.

Unlike all other kinds of unit tests, iPhone application tests aren't run during the build phase. You must build and run your test application, which downloads both it and the unit tests to your iPhone for execution. This introduces a number of limitations to using iPhone application tests:

  • Application tests can't be made an automatic part of your build process.

  • The application test bundle must also be provisioned to run on your iPhone. The "correct" way to do this is to create a provisioning profile that includes both the application and the application test bundle (see Chapter 22). I admit that I'll often simply set the Bundle

  • Identifier build setting in the unit test bundle to the same ID as the application. It seems sleazy, but it works.

  • The unit test bundle will take over your app, run its tests, and exit. You can't use your application interactively during testing.

  • The code in the unit test can't link directly to the application. This is because the unit test target builds before the application, so it can't link directly to the application's code.

You might be scratching your head about the last one. You're probably asking "If the unit test code can't link to the code in the application, what use is it?"

One solution is to include the code in both targets. At run time only one implementation of the class will be used — most likely the one in the application (because it loaded first), but the Objective-C run time doesn't specifically guarantee this. Regardless, this is an acceptable solution in most cases and gives your unit tests direct access to iPhone hardware and its application environment.

Another solution is introspection. Instead of referring to application classes directly, do it indirectly in the case where the test will be running on an actual iPhone. Listing 20-1 shows an example. This code will compile, link, and run — as long as something in the same process actually implements the SieveOfEratosthenes class, which our application does.

Example 20-1. Using soft class references in an iPhone application test

- (void)setUp
{
#if TARGET_OS_IPHONE
    testSieve = [[NSClassFromString(@"SieveOfEratosthenes") alloc]
                 initWithMaxPrime:UNIT_TEST_MAX_PRIMES];
#else
    testSieve = [[SieveOfEratosthenes alloc]
                 initWithMaxPrime:UNIT_TEST_MAX_PRIMES];
#endif
    STAssertNotNil(testSieve,@"Unable to create SieveOfEratosthenes");
}

Warning

The most significant pitfall in iPhone application testing is the same problem inherent in logic tests (independent unit tests). Namely, that you run the risk of testing code that's different from the code in your final product. You must remember to update your test app target scrupulously so that it has the same build configuration as your primary app target. If you have any doubts, simply discard the test app target and reproduce it using the steps listed previously. This will guarantee that all of your test app target settings are identical to those in your production app target.

CREATING A UNIT TEST

Once you have your unit test target created and configured, adding unit tests is simple. Here are the basic steps:

  1. Create a unit test class and add its source file to the unit test target.

  2. Add test methods to the class.

  3. Register the tests with the unit testing framework.

Unit test class files can go anywhere in your project, but I suggest, at the very least, creating a group for them named "Tests" or "Unit Tests." In a larger project you might organize your unit test files in a folder, or you might group them together with the code that they test. The choice is yours.

Each class that you create defines a group of tests. Each test is defined by a test method added to the class. A class can contain as many different tests as you desire, but must contain at least one. How you organize your tests is entirely up to you, but good practices dictate that a test class should limit itself to testing some functional unit of your code. It could test a single class or a set of related functions in your application.

Once defined, your tests must be registered with the unit testing framework so that it knows what tests to run. For Objective-C tests this happens automatically. Objective-C test methods must adhere to a simple naming scheme — basically they must all begin with the name "test." Objective-C introspection is then used to locate and run all of the tests you defined. For C++ unit tests, you add a declaration for each test you've written. The exact requirements for each are described in the "Objective-C Tests" and "C++ Test Registration" sections, respectively.

Each test method should perform its test and return. Macros are provided for checking the expectations of each test and reporting failures. A test is successful if it completes all of its tests and returns normally. An example test is shown in Listing 20-2.

Example 20-2. Sample C++ unit test

void SieveOfEratosthenesTests::testPrimes( )
{
    // Test a number of known primes
    static int knownPrimes[] =
       { 2, 3, 5, 11, 503, 977, 12347, 439357, 101631947 };

    SieveOfEratosthenes testSieve(UNIT_TEST_MAX_PRIMES);
    for (size_t i=0; i<sizeof(knownPrimes)/sizeof(int); i++)
       CPTAssert(testSieve.isPrime(knownPrimes[i]));
}

In this example, the testPrime function defines one test in the SieveOfEratosthenesTests class. The test creates an instance of the SieveOfEratosthenes class, and then checks to see that it correctly identifies a series of numbers known to be prime. If all of the calls to testSieve.isPrime() return true, the test is successful; the testPrimes object is destroyed and the function returns. If any call to testSieve.isPrime() returns false, the CPTAssert macro signals to the testing framework that the test failed. The testing macros are described in the "Objective-C Test Macros" and "C++ Test Macros" sections.

Common Test Initialization

If a group of tests — defined as all of the tests in a TestCase class — deal with a similar set of data or environment, the construction and destruction of that data can be placed in two special methods: setUp and tearDown. The setUp method is called before each test is started, and the tearDown method is called after each test is finished. Override these methods if you want to initialize values or create common data structures that all, or at least two, of the tests will use. The typical use for setUp and tearDown is to create a working instance of an object that the tests will exercise, as illustrated in Listing 20-3. The test class defines a single instance variable that is initialized by setUp and destroyed by tearDown. Each test is free to use the object in the instance variable as the subject of its tests.

For Objective-C tests, the methods your test class should override are -(void)setUp and -(void)teardown. For C/C++ tests, the functions to override are void TestCase::setup() and void TestCase::tearDown().

Example 20-3. Objective-C unit test using setUp and tearDown

SieveOfEratosthenesTests.h
#define UNIT_TEST_MAX_PRIMES  100000

@interface SieveOfEratosthenesTests : SenTestCase
{
    SieveOfEratosthenes* testSieve;
}
SieveOfEratosthenesTests.m
@implementation SieveOfEratosthenesTests

- (void)setUp
{
    testSieve = [[SieveOfEratosthenes alloc]
                 init:UNIT_TEST_MAX_PRIMES];
}

- (void)tearDown
{
    [testSieve release];
    testSieve = nil;
}

- (void)testInvalidNumbers
{
    // These should all return NO
    STAssertFalse([testSieve isPrimeInMap:-1],
                  @"-1 is not a prime number");
    STAssertFalse([testSieve isPrimeInMap:0],
                  @"0 is not a prime number");
    STAssertFalse([testSieve isPrimeInMap:1],
                  @"1 is not a prime number");
}

The setUp and tearDown methods are called before and after every test. This allows tests to perform destructive tests on the object — that is, tests that alter the object's state — because the object will be destroyed at the end of the test and re-created anew before the next test is run.

Unit test classes have standard constructor and destructor methods. Do not use the constructor to create test data. If your test structures are expensive to create and destroy, you may be tempted to create them in the constructor and let them persist for the duration of the test class. Don't do this. Your next approach might be to turn the setUp method into a factory that creates a singleton object when called the first time. Sorry, but that probably won't work either. Some testing frameworks create a separate instance of the test class for each test.

Instead, make a single test that creates the expensive object and then calls a series of subtests itself. Remember not to name your Objective-C subtests "test..." or the testing framework will run them again.

Because so many of the minor details of creating tests for Objective-C and C/C++ differ, the steps for creating your own tests have been separated into the following two sections, "Objective-C Tests" and "C++ Tests."

Objective-C Tests

To create an Objective-C test class and add it to a unit test target, start by selecting the File

Objective-C Tests
FIGURE 20-3

Figure 20-3. FIGURE 20-3

Click the Next button and give the test case class and file a name. The name should be descriptive of its purposes, such as StudentTests for a set of tests that validate the Student class. Make sure you create a matching .h file. Select the working project and add the test to the desired unit test target, as shown in Figure 20-4, making sure you don't include the test class file in any other target. Click the Finish button.

FIGURE 20-4

Figure 20-4. FIGURE 20-4

Xcode creates a skeletal test class definition and implementation, similar to the one shown in Listing 20-4. All of your test classes for Objective-C must be direct subclasses of the SenTestCase class.

Example 20-4. Example Objective-C test case

#import <SenTestingKit/SenTestingKit.h>

@interface StudentTests : SenTestCase {

}

@end

Xcode has created the framework for your class and has already added it to your unit test target. The only thing you need to do now is to write one or more test methods. Each test method:

  • Must begin with "test" in lowercase, as in -testNegativeCoordinates

  • Must return void

  • Must not take any parameters

An example of three such tests is shown in Listing 20-5.

Example 20-5. Example Objective-C tests

#import "StudentTests.h"

@implementation StudentTests

- (void)setUp
{
    student = [[Student alloc] initWithName:@"Jane Doe"];
    STAssertNotNil(student,@"Unable to create Student");
}

- (void)tearDown
{
    [student release];
    student = nil;
}


- (void)testNameConstructor;
{
    STAssertTrue([[student name] isEqualToString:@"Jane Doe"],
                 @"Student.name property incorrect");
}

- (void)testNamePartsParsing;
{
    STAssertTrue([[student firstName] isEqualToString:@"Jane"],
                 @"Student.firstName parsing incorrect");
    STAssertTrue([[student lastName] isEqualToString:@"Doe"],
                 @"Student.lastName parsing incorrect");
}

- (void)testNamePartsReplacement;
{
    STAssertTrue([[student name] isEqualToString:@"Jane Doe"],
                 @"Student.name property incorrect");

    [student setFirstName:@"John"];
    STAssertTrue([[student name] isEqualToString:@"John Doe"],
                 @"Student.name first name replacement incorrect");

    [student setLastName:@"Smith"];
    STAssertTrue([[student name] isEqualToString:@"John Smith"],
                 @"Student.name last name replacement incorrect");
}
@end

Amazingly, you are all done. The introspective nature of Objective-C allows the unit test framework to discover automatically all classes that are subclasses of SenTestCase in the bundle, and then find all void methods that begin with the name "test." The unit test framework creates your test object and then executes each test method one at a time.

Objective-C Test Macros

When you write your test, you will employ a set of macros to evaluate the success of each test. If the assertion in the macro fails to meet the expectation, the test fails and a signal with a description of the failure is passed back to the unit test framework. Test failures appear as an error in the build log.

Each macro accepts a description of the failure. The description argument is a Core Foundation format string that may be followed by a variable number of arguments, à la NSLog(description,...) or -[NSString stringWithFormat:format,...]. The unit test macros available are listed in the following table. The STFail macro unconditionally records a failed test. Use it in a block of code where the program flow has already determined that a failure has occurred. All of the other macros are assertion macros. The test is successful if the parameters meet the expectations of the assertion. If they do not, a failure is recorded using the description constructed using the format string (@"..." in the table) and the remaining arguments.

UNIT TEST ASSERTION MACRO

DESCRIPTION

STFail(@"...",...)

This is the basic macro for unconditionally recording a test failure. This macro always causes the described failure to be recorded.

STAssertTrue(expression,@"...",...)

The test is successful if the statement expression evaluates to YES. Otherwise, a description of the failed test is logged.

STAssertFalse(expression,@"...",...)

The test is successful if the statement expression evaluates to NO.

STAssertNil(reference,@"...",...)

The test is successful if the statement reference evaluates to nil.

STAssertNotNil(reference,@"...",...)

The test is successful if the statement reference evaluates to something other than nil.

STAssertEquals(left,right,@"...",...)

The test is successful if the numeric value of the statement left equals the numeric value of the statement right. Both statements must evaluate to the same primitive type. That is, they must both be long int, float, and so on. You may cast them if necessary. If the values are not the same type or value, the test fails.

STAssertEqualsWithAccuracy(left,right, accuracy,@"...",...)

The test is successful if the absolute difference between the numeric value of the left statement and the numeric value of the right statement is equal to or less than the value of accuracy. Both left and right must evaluate to the same primitive type. If the values differ by more than accuracy or are not the same type, the test fails.

STAssertEqualObjects(left,right,@"...", ...)

The test is successful if the object reference in the left statement is equal to the object reference in the right statement, according to the [left isEqual:right] method. Both object references must be of the same type and the isEqual method must return normally with a Boolean result. If the object references are not the same type, the isEqual method returns NO, or the isEqual method throws an exception, the test fails.

STAssertThrows(statement,@"...",...)

The test is successful if statement causes an exception to be thrown.

STAssertThrowsSpecific(statement,class,@"...",...)

The test is successful if statement causes an exception of the class class to be thrown.

STAssertThrowsSpecificNamed(statement, class,name,@"...",...)

The test is successful if the statement causes an exception of class with the name exception name to be thrown.

STAssertNoThrow(statement,@"...",...)

The test is successful if statement does not cause an exception to be thrown.

STAssertNoThrowSpecific(statement,class,@"...",...)

The test is successful if statement does not cause an exception of class to be thrown. Note that the test is still successful if the statement causes some other class of exception to be thrown.

STAssertThrowsSpecificNamed(statement, class,name,@"...",...)

The test is successful if statement does not cause an exception of class with the name exception name to be thrown.

After you have added your tests, you are ready to build the unit test target.

C++ Tests

To create a C++ test class, follow the instructions for adding an Objective-C Test Case Class to your project, with the one exception that you'll start by choosing the C++ Test Case Class template from the Carbon group. Once you've selected the correct targets and added the class files to the project, return here.

Note

Even if your application is written in pure C, the C/C++ testing framework still requires C++ objects to define and drive the test process. Write your tests by creating the appropriate C++ class. The test member functions you add can then call your application's C functions.

Xcode creates a skeletal test class definition and implementation, as shown in Listing 20-6. All of your test classes for C++ must be direct subclasses of the TestCase class.

Example 20-6. Example C++ test case

#include <CPlusTest/CPlusTest.h>

class StudentTests : public TestCase {
public:
    StudentTests(TestInvocation* invocation);
    virtual ~StudentTests();
};

Xcode has created the framework for your class and has already added it to your unit test target. The only thing you need to do now is to write one or more test methods. Each test method:

  • Must return void

  • Must not take any parameters

Unlike Objective-C, C++ test method names do not have to conform to any naming convention, but it is more readable if you retain the habit of starting each method name with "test." An example of two such tests is shown in Listing 20-7.

Example 20-7. Example C++ tests

#include "StudentTests.h"

StudentTests::StudentTests(TestInvocation *invocation)
    : TestCase(invocation)
{
}
StudentTests::~StudentTests()
{
}

void StudentTests::testNameConstructor( )
{
    Student student("Jane Doe");
    CPTAssert(strcmp(student.getName(),"Jane Doe")==0);
}

void StudentTests::testNameProperty( )
{
    Student student();
    CPTAssert(student.getName()==NULL)
    student.setName("Jane Doe");
    CPTAssert(strcmp(student.getName(),"Jane Doe")==0);
}

C++ Test Registration

C++ does not include the kind of introspection that Objective-C uses to discover the test classes and methods that you've defined. Consequently, you must tell the C++ unit test framework exactly what tests you've defined. You accomplish this by registering the tests using static constructors, as shown in Listing 20-8.

Example 20-8. C++ test registration

StudentTests studentTestsNameConstructor(TEST_INVOCATION(StudentTests,
                                                        testNameConstructor));
StudentTests studentTestsNameProperty(TEST_INVOCATION(StudentTests,
                                                      testNameProperty));

For every test you want run, you must create an instance of your test class, passing a TestInvocation object to its constructor. To make this easier to code, the unit test framework provides a TEST_INVOCATION macro, which creates a configured instance of the TestInvocation class for you. The macro parameters are the name of your TestCase subclass and the test function. You can give the static variable any name you want, but it is more readable if you give it a name that describes the test. Remember that these object names are public, so generic names like test1 are likely to collide with similar names from other TestCase classes.

Each invocation object is constructed when the application starts up. The constructor for the TestCase class registers the test with the unit test framework. Thus, as soon as the application is ready to run, the testing framework has a complete list of the tests to be executed.

C++ Test Macros

When you write your test, you will employ the CPTAssert macro to evaluate the success of each test. If the argument to the macro evaluates to a non-zero value, the test was successful. If not, the test and a signal with a description of the failure is passed back to the unit test framework. Test failures appear as an error in the build log. Examples of using CPTAssert were shown in Listing 20-7.

C++ Test Execution

In addition to registering the tests, a C/C++ application being tested by a dependent unit test needs to invoke the unit tests at the appropriate time. Unlike an Objective-C application, C applications can't be automatically intercepted to prevent their normal execution. You must add code to your application to run the tests and exit — but only when your application is being run for the purposes of unit testing. This section describes the code you need to add to non-Carbon applications — that is, any kind of process that doesn't use a Carbon run loop — and a less invasive method you can use with Carbon (run loop) applications.

For command-line applications, this is simply a matter of inserting some code into your main() function. You insert this code after the point in your application where your unit tests can be run — typically after any required initialization — but before the application actually starts running. Assuming your application has no special initialization, the example in Listing 20-9 shows what you need.

Example 20-9. Unit test hook for main()

// Conditional support for C/C++ unit tests
#ifndef UNIT_TEST_SUPPORT
#define UNIT_TEST_SUPPORT   1
#endif

#if UNIT_TEST_SUPPORT
#include <CPlusTest/CPlusTest.h>
#endif

int main (int argc, char * const argv[])
{
    //Perform any required initialization here...

#if UNIT_TEST_SUPPORT
    TestRun run;
    // Create a log for writing out test results
    TestLog log(std::cerr);
    run.addObserver(&log);
    // Get all registered tests and run them.
    TestSuite& allTests = TestSuite::allTests();
    allTests.run(run);
    // If the TestSuite ran any tests, log the results and exit.
    if (run.runCount())
        {
        // Log a final message.
        std::cerr << " Ran " << run.runCount()
                  << " tests, " << run.failureCount() << " failed."
                  << std::endl;
        return (0);
        }
    // Otherwise, run the application normally.
#endif
...

The code creates a TestRun object, gets all of the registered tests, and then runs them. In a dependent unit test, the registration code for the tests exists in the unit test bundle. Unless the unit test bundle was loaded and initialized before the startup code called main(), there will be no tests to run. In this case, the application assumes that it is running in the absence of the unit test bundle, falls through, and executes normally. If tests are registered and run, the code reports the success or failure of those tests and exits immediately.

Warning

The definitions of the TestRun, TestLog, TestSuite, and related classes are included in the special unit testing framework that was added to your system when you installed the Xcode Developer Tools. These classes do not normally exist in a standard installation of Mac OS X, and your application will fail to start without them. Do not include this testing code in your final application. Ensure that the release build of your application is devoid of any references to the unit testing classes and framework. Using the previous example, the Release build configuration of this project could define the UNIT_TEST_SUPPORT=0 preprocessor macro to ensure that no unit test code is compiled in the final version.

For Carbon applications, you can intercept the start of program execution far more elegantly. Any technique (like the one just demonstrated) that runs the unit tests after initialization, but before regular program execution, is acceptable. The dynamic nature of the Carbon event loop, however, allows your unit testing bundle to intercept the execution of the application without making any changes to the application itself. This avoids having to add conditionally compiled code to your application.

Listing 20-10 shows how to use a Carbon timer to accomplish this. This code should be added to the unit test target, not your application.

Example 20-10. Unit test hook for Carbon application

UnitTestRunner.h
#include <Carbon/Carbon.h>

class UnitTestRunner
{
private:
    EventLoopTimerUPP timerUPP;
    EventLoopTimerRef timerRef;

public:
    UnitTestRunner();
    virtual ~UnitTestRunner();

protected:
    static void testTimerFired(EventLoopTimerRef timer, void* userData);
    void runTests(EventLoopTimerRef timer);
};
UnitTestRunner.cpp
  #include <CPlusTest/CPlusTest.h>
  #include "UnitTestRunner.h"

  UnitTestRunner installTimer;    // static constructor to create timer

  UnitTestRunner::UnitTestRunner() : timerUPP(NULL), timerRef(NULL)
  {
      // Get the UPP for the static bridge method.
      timerUPP = NewEventLoopTimerUPP(UnitTestRunner::testTimerFired);
      (void)InstallEventLoopTimer(GetMainEventLoop(),0,0,timerUPP,this,&timerRef);
  }

  UnitTestRunner::~UnitTestRunner()
  {
      // Destroy the timer
      if (timerRef != NULL) {
          RemoveEventLoopTimer(timerRef);
          timerRef = NULL;
          }
      if (timerUPP != NULL) {
          DisposeEventLoopTimerUPP(timerUPP);
          timerUPP = NULL;
          }
  }

  // Static method to bridge the call to the local instance.
  void UnitTestRunner::testTimerFired(EventLoopTimerRef timer, void* userData)
  {
      ((UnitTestRunner*)userData)->runTests(timer);
  }

  void UnitTestRunner::runTests(EventLoopTimerRef timer)
  {
      if (timer == timerRef) {
          // We're done with the timer
          RemoveEventLoopTimer(timerRef);
          timerRef = NULL;
          // Create the test run
          TestRun run;
          // Create a log for writing out test results
          TestLog log(std::cerr);
          run.addObserver(&log);
          // Get all registered tests and run them.
          TestSuite& allTests = TestSuite::allTests();
          allTests.run(run);
          // If tests were run, log the results and terminate the application
          if (run.runCount())  {
              // Log a final message.
              std::cerr << " Ran " << run.runCount() << " tests, "
                        << run.failureCount() << " failed."
                        << std::endl;
              QuitApplicationEventLoop();
              }
          // Else, fall through and continue running the application
          }
}

The static constructor for installTimer causes an instance of this object to be created during the initialization of your application. Because the constructor is part of the code in the unit test, the UnitTestRunner object is only created when your application is running in the presence of the unit test bundle. The class creates a timer with a 0 interval and registers it with the Carbon event manager. As soon as the application has performed its basic initialization and the event loop is ready to run, the timer fires. The testTimerFired() function catches the timer event and invokes the runTests() function. This function runs all of your unit tests and quits the application.

In the absence of the unit test bundle, there is no constructor, no timer object is created, and your application starts running normally. The beauty of this scheme is that it requires no modification to your application. There is no possibility of accidentally producing a version of your application that contains any unit test support, and you can test the final application binary you intend to release.

DEBUGGING UNIT TESTS

Who watches the watchers? Sometimes a unit test, designed to keep your application free of bugs, has bugs itself. When this occurs, you need to bring the power of the debugging tools to bear on the unit test code, rather than your application. The problem is that unit tests run during the build phase, not the debug phase, of the Xcode environment. The tests themselves are never the target of a Debug or Run command, and you have the added catch-22 of trying to build an application whose unit tests fail.

Debugging iPhone Application Tests

However awkward an iPhone application (dependent) unit test is to set up and use, it is stunningly simple to debug.

Remember that an iPhone application test is a regular copy of your iPhone app that includes a unit testing bundle. To debug your application tests, simply run your test application target under the control of the debugger (Run

Debugging iPhone Application Tests

Debugging Dependent Mac OS X Unit Tests

Debugging dependent unit tests for an application requires that you reverse the normal order of targets and trick the application target into running your unit tests instead of executing your application normally, all under the control of the debugger. Here's how:

  1. Open the Info window for the project (Project

    Debugging Dependent Mac OS X Unit Tests
  2. Remove the application target dependency from the unit test target.

  3. Set the active target to the application target and build it (Build

    Debugging Dependent Mac OS X Unit Tests
  4. Add the unit test target as a dependency for the application. This reverses the normal dependency between the unit test and the target.

  5. Disable the run script phase of the unit test target. Expand the dependent unit test target and double-click the final run script phase. Edit the script by adding an exit command at the beginning, as shown in Figure 20-5, essentially disabling the script.

    FIGURE 20-5

    Figure 20-5. FIGURE 20-5

  6. Open the Info window for the application's executable (in the Executable smart group). Select the Arguments tab. If this is an Objective-C application, add the argument -SenTest All.

  7. In the environment variables pane:

    1. Add a DYLD_INSERT_LIBRARIES variable and set its value to $(DEVELOPER _LIBRARY_DIR)/PrivateFrameworks/DevToolsBundleInjection .framework/DevToolsBundleInjection.

    2. Add a DYLD_FALLBACK_FRAMEWORK_PATH variable and set it to $(DEVELOPER _LIBRARY_DIR)/Frameworks.

    3. Add an XCInjectBundle variable and set it to UnitTestBundlenName.octest. If this is a C++ testing bundle, the extension will be .cptest instead of .octest.

    4. Add an XCInjectBundleInto variable and set it to AppName.app/Contents/MacOS/AppName.

  8. Your application executable should now look like the one in Figure 20-6. Set your active build configuration to Debug.

    FIGURE 20-6

    Figure 20-6. FIGURE 20-6

  9. Add the statement set start-with-shell 0 to the invisible .gdbinit file in your home directory, creating the file if it doesn't already exist. You can do this using a number of text editors (BBEdit, emacs, pico, and so on) or by issuing the following commands in a Terminal window:

    echo '' >> ~/.gdbinit
    echo 'set start-with-shell 0' >> ~/.gdbinit

Now build and debug your project. The application target causes the unit tests target to build, but not run (because you disabled the unit test's run script). Note that there's a quirky circular dependency here — the unit test target still tries to link to your application's binary. If your application hasn't been built, the unit test target will fail to build with a link error. That's why I had you perform step 3 before proceeding, so the unit test bundle has a product to satisfy its link phase.

Xcode then launches the executable under the control of the debugger. The environment variables trick the application executable into acting as if it were being run for the purposes of unit testing. The system loads the unit test bundle and executes the tests, allowing you to set breakpoints in the unit test source and debug them.

When you are done debugging your unit tests, reverse the entire process, as follows:

  1. Reverse the dependencies so that the unit test target once again depends on the application target.

  2. Remove or comment out the exit statement at the beginning of the unit test's run script phase.

  3. Disable the special arguments and environment variables in the application's executable by removing the check mark next to each one.

  4. Set your project's Place Intermediate Build File In setting back to your original choice.

  5. Remove the set start-with-shell statement in your ~/.gdbinit file. You can optionally just leave it, because it shouldn't interfere with anything else in Xcode.

Debugging Independent Unit Tests

Debugging independent tests is a little simpler. It requires that you create an executable from the unit test bundle so that the debugger knows how to launch it under its control. The executable is actually the test harness utility and its arguments tell it what unit test bundle to load and execute. Follow these steps to create the executable:

  1. Create a custom executable. Give it the same name as your independent unit test target. (The "Custom Executables" section in Chapter 18 has the details.)

  2. Set the executable to $(DEVELOPER_TOOLS_DIR)/otest for Objective-C unit test bundles, or $(DEVELOPER_TOOLS_DIR)/CPlusTestRig for C++ unit test bundles.

  3. In the Arguments tab, add the $(CONFIGURATION_BUILD_DIR)/NameOfUnitTest.octest argument for Objective-C bundles. The bundle extension will be .cptest for C++ bundles.

  4. Disable the unit tests in the independent unit test target by adding an exit statement to the beginning of the run script phase, as previously shown in Figure 20-5.

Set the active target to the unit test and the active executable to the custom executable you just created. You can now set breakpoints in the unit tests and launch it under the control of the debugger just like any other application. When you are done debugging, restore the run script phase of the unit tests by removing the exit statement in the run script phase. This permits the tests to run during the build phase once again.

SUMMARY

Unit tests can be powerful allies. They permit you to codify the behavior of your code and integrate the validation of that behavior directly into the build process. Incorrect handling of a parameter value, or the failure to throw an exception under certain circumstances, is now as easily caught as syntax errors in your source code.

Effective use of unit testing is a discipline, encompassing a variety of philosophies and a broad range of styles and techniques. If you are serious about integrating unit testing into your development, you should get a good book on test-driven design or Extreme Programming.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset