C. Keith Ray

C. Keith Ray writes about and develops software in multiple platforms and languages, including iOS® and Macintosh®.
Keith's Résumé (pdf)

Wednesday, January 9, 2013

Basic Unit Testing in Objective-C


When you create an iOS or MacOSX project in Xcode, you have the option of creating unit tests, which you can use to verify your code without manual intervention.

Here's an example...


//  IntroductionToObjectiveCTests.m
//
//  Created by C. Keith Ray on 1/9/13.
//  Copyright (c) 2013 C. Keith Ray. All rights reserved.
//

#import <SenTestingKit/SenTestingKit.h> // Testing Framework

// Note 1
@interface IntroductionToObjectiveCTests : SenTestCase
@end

// Note 2
@implementation IntroductionToObjectiveCTests

// Note 3
- (void)testExampleTrue
{
    STAssertTrue(TRUE, @"TRUE should be true");
}

// Note 4
- (void)testExampleFalse
{
    STAssertTrue(FALSE, @"This assert should fail");
}

@end



If we build and run the tests, Xcode will compile and run these tests.

Note 1: Declares a class that has SenTestCase as its parent class. Nothing else needs to go here.

Note 2: Defines the class we declared in Note 1.

Note 3: This is a method definition, or a "member function" if you use C++ terminology, that the test framework will execute. It has to return nothing ("void"), take no parameters, and has to start with the word "test" for the test framework to find and execute this method. I call this "a test" and this file has two tests in it, so far. This test contains an assertion that will pass, because TRUE is true.

Note 4: This test will fail, because FALSE is not true and the assertion STAssertTrue expects the first parameter to be evaluated to true.

The the tests are executed, Xcode will highlight the lines of code where the failing assertions are. We have just one failure; it looks like this:


The failure output tells us the expression FALSE is expected to be true (in this test), but is actually not true. The output also includes the string supplied as the second argument to STAssertTrue.

When I had Xcode generate the project and create the initial unit test code, it created two files: IntroductionToObjectiveCTests.m and IntroductionToObjectiveCTests.h. A header file (.m) to declare the interface of an objective-c test case, and a source file (.m) implementing the test case. I moved the class interface declaration to the .m file and deleted the .h file, because we don't need it. The .h would only be needed if I had written code in multiple .m files that needed to see the class interface declaration.

Why doesn't the test framework need to see the class interface? Magic. When the test code is executed, the test framework looks for classes and methods at runtime. The framework says something like "Give me a list of all your classes that have SentTestCase as their parent class, and give me the list of methods that start with 'test'. Let's execute those test methods."

SenTestingKit also outputs information to the Console. This is what it looks like for these tests:


Test Suite 'All tests' started at 2013-01-09 20:21:02 +0000
Test Suite '/Users/keithray/Library/Developer/Xcode/DerivedData/intro_objc-gfshipzbnghwbygbtraflyelmail/Build/Products/Debug-iphonesimulator/intro_objcTests.octest(Tests)' started at 2013-01-09 20:21:02 +0000
Test Suite 'IntroductionToObjectiveCTests' started at 2013-01-09 20:21:02 +0000
Test Case '-[IntroductionToObjectiveCTests testExampleFalse]' started.
/Users/keithray/projects/NSScreenCast/01_objective_c_language/intro_objc/intro_objcTests/IntroductionToObjectiveCTests.m:25: error: -[IntroductionToObjectiveCTests testExampleFalse] : "FALSE" should be true. This assert should fail
Test Case '-[IntroductionToObjectiveCTests testExampleFalse]' failed (0.000 seconds).
Test Case '-[IntroductionToObjectiveCTests testExampleTrue]' started.
Test Case '-[IntroductionToObjectiveCTests testExampleTrue]' passed (0.000 seconds).
Test Suite 'IntroductionToObjectiveCTests' finished at 2013-01-09 20:21:02 +0000.
Executed 2 tests, with 1 failure (0 unexpected) in 0.000 (0.000) seconds
Test Suite '/Users/keithray/Library/Developer/Xcode/DerivedData/intro_objc-gfshipzbnghwbygbtraflyelmail/Build/Products/Debug-iphonesimulator/intro_objcTests.octest(Tests)' finished at 2013-01-09 20:21:02 +0000.
Executed 2 tests, with 1 failure (0 unexpected) in 0.000 (0.000) seconds
Test Suite 'All tests' finished at 2013-01-09 20:21:02 +0000.
Executed 2 tests, with 1 failure (0 unexpected) in 0.000 (0.002) seconds

If you had a lot of test failures, one way to find them would be to search the console for "failed", "error:" and so on.