Unit Testing a Bundle in Xcode, Part 2
In a previous episode, I tried to setup Xcode to run unit tests from a bundle. While it worked, it didn't work in all cases. First off, I probably should have used load
instead of initialize
. But even that doesn't help. The problem is that loading the bundle before tests run works only when you reference functions or Objective-C classes from your test case. Access a plain-old ordinary C symbol, and you're hosed. This happens, for example, when using constants for notification names, which would be defined in a header like:
extern NSString * const MyNotification;
If you now try and use MyNotification
from within a test, and build the unit test target, you'll get a runtime link edit error along the lines of: "Symbol not found: _MyNotification" (the compiler always adds an underscore in front of all symbol names). This is because functions are bound lazily, and global variables are not. Or more precisely, global variables cannot be loaded lazily. Extrapolating from information in Tech Note 2064, and after thinking about this, it makes sense. The dynamic link editor can setup stubs for functions: e.g. func()
calls dyld_stub_func()
, which then finds the _func
symbol, and calls it. That's not really how it's done, but it's close (do an "nm <executble> | grep dyld"
sometime). This level of indirection cannot be done for global variables (or function pointers).
Okay, so how to solve this. We need to find a way to get the global symbols into the unit test bundle before the unit tests run. It is possible, but it requires a little more work than last time. Xcode uses a trick of setting the DYLD_INSERT_LIBRARIES
dyld environment variable to force loading a library before an application is run. Unfortunately, this cannot be used to load a bundle. I couldn't figure out how to get the OCUnit otest
tool to do it, either. So I had to write my own unit test rig, which I'm calling BundleTestRunner
.
In your project, create a new Cocoa shell tool target named BundleTestRunner
. There are some build settings that need to be changed, but first the code:
#import <SenTestingKit/SenTestingKit.h> int main(int argc, char * argv[]) { NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; NSEnumerator * arguments = [[[NSProcessInfo processInfo] arguments] objectEnumerator]; // Skip argv[0] [arguments nextObject]; NSString * bundlePath; NSString * lastBundlePath; while (bundlePath = [arguments nextObject]) { NSBundle * bundleToLoad = [NSBundle bundleWithPath: bundlePath]; if ([bundleToLoad load]) { NSLog(@"Loaded bundle: %@", bundlePath); lastBundlePath = bundlePath; } } SenTestSuite * suite; suite = [SenTestSuite testSuiteForBundlePath: lastBundlePath]; BOOL hasFailed = ![[suite run] hasSucceeded]; [pool release]; return ((int) hasFailed); return 0; }
This tool takes a list of bundles as command line arguments. It tries to load each of them, and then it runs all tests found in the last one. To run this on your unit test bundle, you would run it like:
BundleTestRunner .../MyBundle.bundle .../MyUnitTests.octest
There is one final issue to overcome: two-level namespaces. Two-level namespaces were created in OS X 10.2 to reduce symbol conflicts between loaded bundles. This means if an app loads two bundles, they could both define a symbol with the same name without a link error (so long as the main app doesn't try to reference it). This causes problems for BundleTestRunner
. With two-level namespaces, the MyNotification
symbol loaded from MyBundle.bundle
is not available to MyUniTests.octest
. This is normally a Good Thing, but in this case, two-level namespaces need to be disabled. One way to do this is to set the DYLD_FORCE_FLAT_NAMESPACE
dyld environment variable. But a better option is to link BundleTestRunner
with -force_flat_namespace
linker option.
On to the build settings. Open up the build properties for the target, and turn off the "ZeroLink" setting (it's probably on for the Debug configuration). Next Add -framework SenTestingKit
and -force_flat_namespace
to "Other Linker Flags". This adds a reference to SenTestingKit
and disables two-level namespaces, as previously discussed.
That's all for BundleTestRunner
. Now create unit a test bundle, as usual. Add a dependency to BundleTestRunner
, as well as to the bundle you'd like to test. After adding tests, building will cause errors, though, because it's still using the Xcode test runner, instead of BundleTestRunner
. Change the script in the "Run Script Phase" to:
cd ${BUILT_PRODUCTS_DIR} ./BundleTestRunner MyBundle.bundle ${WRAPPER_NAME}
This causes BundleTestRunner
to load MyBundle.bundle
and WRAPPER_NAME
, and the run all tests in WRAPPER_NAME
. Finally, that's all! You should now be able tun run the unit tests when building the unit test target.