http://code.google.com/p/google-toolbox-for-mac/wiki/iPhoneUnitTesting
Introduction
This is a quick tutorial on doing iPhone unit testing using the facilities in the Google Toolbox For Mac. Please send mail to the group if any clarification is required.
Basic Project Setup
Hopefully your project already has an application target for building something for the iPhone.
- Create a new iPhone Target of type “Cocoa Touch Application” via “Project Menu > New Target…”. Choose a name that makes some sense such as “Unit Test”. Be sure to use a “Cocoa Touch Application” target as opposed to a “Cocoa Application” target or a “Cocoa Touch Static Library” target or “Cocoa Touch Unit Test Bundle”.
- Add google-toolbox-for-mac/UnitTesting/GTMIPhoneUnitTestMain.m to your target
- Add google-toolbox-for-mac/UnitTesting/GTMIPhoneUnitTestDelegate.m to your target
- Add google-toolbox-for-mac/UnitTesting/GTMIPhoneUnitTestDelegate.h to your project
- Add google-toolbox-for-mac/UnitTesting/GTMSenTestCase.m to your target
- Add google-toolbox-for-mac/UnitTesting/GTMSenTestCase.h to your project
- Add google-toolbox-for-mac/UnitTesting/GTMUnitTestDevLog.m to your target
- Add google-toolbox-for-mac/UnitTesting/GTMUnitTestDevLog.h to your project
- Add google-toolbox-for-mac/Foundation/GTMObjC2Runtime.m to your target
- Add google-toolbox-for-mac/Foundation/GTMObjC2Runtime.h to your project
- Add google-toolbox-for-mac/Foundation/GTMRegex.m to your target
- Add google-toolbox-for-mac/Foundation/GTMRegex.h to your project
- Add google-toolbox-for-mac/GTMDefines.h to your project
- Add a new ‘run script’ build phase as the last step of your target build via “Project Menu > New Build Phase > New Run Script Build Phase”, and dragging it to the end of the build steps if needed.
- Edit your Run Script Build Phase by double clicking it, and set the shell to “/bin/sh” and the script to"PATH_TO_GTM/UnitTesting/RunIPhoneUnitTest.sh", where PATH_TO_GTM is the path to your local version of google-toolbox-for-mac.
- Xcode’s new application stationery generates an Info.plist that specifies a Main nib file base name. Open the new unit test’s .plist file and remove that line. If you don’t do this, RunIPhoneUnitTest.sh will crash when UIApplication throws an exception trying to load an inappropriate or non-existent nib file.
- Build! Note that if you choose build and go you will see your unit tests executed twice, once as part of the build script, and once being run
Your target should now build cleanly, and if you check the build log you should see something like: “Executed 0 tests, with 0 failures (0 unexpected) in 0.001 (0.001) seconds” at the end.
Trouble Shooting
- Make sure you are not linked against the SenTestingKit.framework.
Creating a unit test
- Add the source you want to test to your target. For example if you want to test class Foo, make sure to add Foo.h and Foo.m to your target.
- Add a new unit test file to your target via “File > New File” and choose “Objective-C test case class” from the “Mac OS X” Cocoa category. Call it FooTest.m, or follow whatever convention your project has for naming test classes.
- In FooTest.h, change #import <SenTestingKit/SenTestingKit.h> to #import "GTMSenTestCase.h"
- Also set up your class so that it inherits from GTMTestCase (not GTMSenTestCase) instead of SenTestCase
@interface MyTestCase : GTMTestCase { ... } - Add test cases as you normally would. See Apple’s Documentation for a good tutorial on how to test in Objective C. The key is that your test case methods are declared - (void)testBar. The name must start with “test” and they must return nothing and have no arguments.
Now when you build your target you should now see test cases executing. You can repeat this process creating additional source files for each class you want to write Unittests for.
Debugging
You can debug your unit test executable the exact same way you would unit test any executable. You shouldn’t have to set up anything. Note that you may see cases where the unit test build fails, but it doesn’t fail when you are debugging. When the unit test build is running several flags are turned on to encourage failures such as MallocScribble, NSAutoreleaseFreedObjectCheckEnabled etc. You may want to look inRunIPhoneUnitTest.sh and see what is being enabled for you.
Notes
- We find that having a .h for tests to be mostly useless, and tend to just the interfaces for the tests in with the implementation so I only have one file to worry about.
- Make sure to check out the extra ST macros that we have added in GTMSenTestCase.h that go above and beyond the set included with the default OCUnit.
- STAssertNoErr(a1, description, …), STAssertErr(a1, a2, description, …)
- STAssertNotNULL(a1, description, …), STAssertNULL(a1, description, …)
- STAssertNotEquals(a1, a2, description, …), STAssertNotEqualObjects(a1, a2, desc, …)
- STAssertEqualObjects(a1, a2, description, …), STAssertEquals(a1, a2, description, …), STAssertEqualsWithAccuracy(a1, a2, accuracy, description, …)
- STAssertOperation(a1, a2, op, description, …), STAssertGreaterThan(a1, a2, description, …), STAssertLessThan(a1, a2, description, …), STAssertLessThanOrEqual(a1, a2, description, …)
- STAssertEqualStrings(a1, a2, description, …), STAssertNotEqualStrings(a1, a2, description, …), STAssertEqualCStrings(a1, a2, description, …), STAssertNotEqualCStrings(a1, a2, description, …)
- STAssertTrueNoThrow(expr, description, …), STAssertFalseNoThrow(expr, description, …), STAssertThrows(expr, description, …), STAssertThrowsSpecific(expr, specificException, description, …), STAssertThrowsSpecificNamed(expr, specificException, aName, description, …), STAssertNoThrow(expr, description, …), STAssertNoThrowSpecific(expr, specificException, description, …), STAssertNoThrowSpecificNamed(expr, specificException, aName, description, …)
- You can’t run the build script while the iPhone simulator is running. The build script does attempt to kill it off before it runs, but if you seeCouldn't register PurpleSystemEventPort with the bootstrap server. Error: unknown error code. This generally means that another instance of this process was already running or is hung in the debugger. or Abort trap "$TARGET_BUILD_DIR/$EXECUTABLE_PAT" -RegisterForSystemEvents you probably need to figure out why the simulator (or another iPhone process) is already running. The exact error has changed with different versions of the iPhone SDK.
Advanced Stuff
Unit test Logging
When Unittesting is done correctly, you often have a lot of log messages logging because you are testing edge cases that you may not expect to hit in the real world very often. It’s nice to be able to verify that the log messages you are receiving as you run your tests are the ones that you expect to receive. You can do this in GTM by enabling unit test logging.
- Assuming you are using _GTMDevLog to do your logging,#define _GTMDevLog _GTMUnittestDevLog somewhere, either in target settings, or in your prefix. If you are using NSLog you can just define it to be _GTMUnittestDevLog.
- Add google-toolbox-for-mac/DebugUtils/GTMDevLog.m to your target.
- Add google-toolbox-for-mac/UnitTesting/GTMUnitTestDevLog.m to your target.
- Add google-toolbox-for-mac/Foundation/GTMRegex.m to your target.
- You may also need to add some headers depending on your search paths
Now when you build all of the logging that you do via your unit tests will get checked to make sure that it conforms with your expectations. You set up these expectations before running your tests using [GTMUnitTestDevLog expect*] methods. See GTMUnitTestDevLog.h for more info.
UI and State Testing
GTM can also help you test your UI’s representation and state.
- Add google-toolbox-for-mac/UnitTesting/GTMNSObject+UnitTesting.m to your target.
- Add google-toolbox-for-mac/UnitTesting/GTMUIKit+UnitTesting.m to your target.
- Add google-toolbox-for-mac/UnitTesting/GTMCALayer+UnitTesting.m to your target.
- Add google-toolbox-for-mac/Foundation/GTMSystemVersion.m to your target.
- Add the CoreGraphics and QuartzCore frameworks to your target.
- You may also need to add some headers depending on your search paths
Check out UnitTesting/GTMUIKit+UnitTestingTest.m for examples of using UIUnitTesting on the iPhone.
For more information on some of this, check out CodeVerificationAndUnitTesting. Hope this helps.
Unit Test Environment Variables
To encourage “bad behavior” by the code being tested, the RunIPhoneUnitTest.sh script sets a variety of environment variables. If you are wondering why the unit tests fail when you are building, but don’t fail when you are running, it may be because of a side effect of one of these variables. Take a look at all the export commands within RunIPhoneUnitTest.sh to see what’s actually going on.
Leaks
By default the iPhone unit tests will run leaks after all the tests have completed. This can be turned off by setting the GTM_DISABLE_LEAKSenvironment variable before you execute the RunIPhoneUnitTest.sh script. Out of the box, NSZombies will also be enabled. This however interferes slightly with leaks and makes it difficult to get good backtraces and context. If you want the backtraces and context, set theGTM_DISABLE_ZOMBIES environment variable before you execute the RunIPhoneUnitTest.sh script. All leaks will appear as warnings on the build console.
Termination
Some of Apple’s tools (such as Instruments) don’t want the app to terminate underneath them. By default the iPhone unit test app will terminate when it has finished it’s run. Set the GTM_DISABLE_TERMINATION environment variable if you want to disable termination and just have the unit test “run” until you are done with it.
Keychain Testing
If your code uses the keychain, you may need to set GTM_DISABLE_IPHONE_LAUNCH_DAEMONS=0 before you run your unit tests. See RunIPhoneUnitTest.sh for details on this flag and why it should be set.