views:

393

answers:

2

Following Chris Hanson's blogs and Apple's Automated Unit Testing with Xcode 3 and Objective-C I have started implementing unit tests for my projects. However, I use a lot of plug-ins (loadable bundles) and I can't quite figure out how to test them.
I figured I would use the approach Chris Hanson described for testing frameworks. I started with a Cocoa Bundle project, added a principal class and changed the type to plugin.
Then I added the unit test bundle, add the plugin as a direct dependency (Apple's instructions) and set the following build settings:

Bundle Loader: $(BUILT_PRODUCTS_DIR)/CocoaPlugin.plugin/Contents/MacOS/CocoaPlugin
Test Host: $(BUNDLE_LOADER)

The problem is that as soon as I've done that and build the test target, I get this message:

error: Test host '/Users/elisevanlooij/Documents/Plug-ins/CocoaPlugin/build/DebugCocoaPlugin.plugin/Contents/MacOS/CocoaPlugin' exited abnormally with code 127 (it may have crashed). [code 126 in another plugin]

I had hoped that adding the otest custom executable would help, but unfortunately not. I really hope someone can help because not being able to unit test my plugin really puts a cramp in my testing lifestyle.

+2  A: 

Take a step back. Your Bundle Loader setting is erroneous and adding a custom executable is not going to affect compilation of a unit-test bundle.

You need to get your unit-test bundle to build without errors (and warnings!), and your tests will run automatically (you do have at least one valid SenTestCase class with at least one valid test method, right?).

So, are you saying that your test-bundle compiles without warnings and you have written some tests using classes and methods from your plugin? If so you must have some how taken care of loading the plugin-bundle into the unit-test-bundle and defining some kind of API, as the plugin-bundle doesn't have any public headers, right?

see Apple docs here

Loading plugins into plugins (essentially what you are trying to do) is not easy and they are not magically 'linked' at compile time like the frameworks in the Chris Hanson Blog that you refer too. They wouldn't be plugins if they were.

The simplest way to go is to not actually test your plugin at all but add the files you want to test directly to the unit-test bundle. At least this way you can get on with testing your code without fiddling about with dynamically loading bundles.

But if this isn't satisfactory, you can get what you are trying to do to work with a little effort - you should definitely add tests to verify that your plugin is loaded and that the symbols you think are available REALLY are available. Once your tests build ok you should follow Chris Hanson's other excellent blog on debugging unit test bundles showing you how to step thru your tests in the debugger - you should be able to track down any errors.

Long answer, but somehow I have the feeling we're at cross-purposes here. My plugins load and work fine, what I want however is to find a way to unit-test their inner workings. And that is exactly what I cannot do with the instructions that are out there, including Mr. Hanson's excellent blogs. The problem is that to the loading program, a plugin is a black box and the debugger can't go inside. So, the unit test must be loaded with the plugin, but how?
Elise van Looij
if you really don't want to compile the plugin classes directly into your unit-test bundle as i suggest you will need to look at + (NSBundle *)bundleWithPath:(NSString *)fullPath to make sure you plugin code is loaded at runtime from your unit-test bundle. Nothing about a plugin means you can't 'go inside' with the debugger. It is not clear if you have written any tests yet but if you have you must be familiar with NSClassFromString, __attribute__((weak)), etc. that you will need to deal with missing symbols at compile time.
I've done some more experimenting in the past days. I went back to Chris Hanson's post on debugging a framework. Using /Developer/Tools/otest as a custom executable gives an error: otest[3991:10b] launch path not accessible.I then substituted an executable that loads the plug-in. The testcase instantiates the principal class of the plugin in -(void)setUp and in -(void)testPlugin does an STAssertNotNil on one of the properties. The problem now is that when STAssertNotNil fails and I add a breakpoint in front of it, the debugger won't break.
Elise van Looij
sounds like progress. It is worth getting otest to work - your arguments are wrong. Is your first 'argument passed on launch' -SenTest All and second argument /theFullPath/to/my/testBundle.octest ? Secondly, debugging. Obvious things first - are you doing a debug build? With all symbols and no optimization? On my ppc machine i swear the option 'GCC_FAST_OBJC_DISPATCH' confuses the debugger.. Finally under xcode preferences make sure 'Load symbols lazily' is unchecked.
Yes, yes for the arguments. Yes, have been using debug build all this time -- don't know about symbols and optimization. I click the yellow spraycan. I did uncheck the lazy loading of symbols, but that doesn't seem to matter.The problem I'm having now is that gdb reports a mismatch between the garbage colleciton on the the plugin and the unit test bundle. Both, however, are set to 'garbage collection supported'.
Elise van Looij
A: 
  1. Your

    Bundle Loader: $(BUILT_PRODUCTS_DIR)/CocoaPlugin.plugin/Contents/MacOS/CocoaPlugin

    is correct. It means that when linking your test bundle you do not include the classes under test there, and they will be looked up from CocoaPlugin. It is a compile time setting and should cause your test bundle to compile/link sucesfully. (See -bundle_loader in man ld)

  2. Your

    Test Host: $(BUNDLE_LOADER)

    is incorrect. Your test host should be either an application (with a NSApplicationMain called from main method) or not set. This TEST_HOST setting is a runtime setting to run your unit tests. You basically have two options:

    • Do not set TEST_HOST, and load your plugin from your test bundle. For example you can do this using the initlaize method.

    • Create a dummy test_host application that will load your plugin, and then call NSApplicationMain, and use this app as your TEST_HOST.

The +initalize method for your test bundle to load the plugin would look like this:

+ (void)initialize
{
    NSBundle* bundle = [NSBundle bundleWithPath:pathToPlugin];
    [bundle load];
    NSLog(@"Loaded:%@\n",bundle);
}

The main method in your dummy_test host app can look like this:

int main(int argc,const char** argv)
{
  NSBundle* bundle = [NSBundle bundleWithPath:pathToPlugin];
  [bundle load];
  NSLog(@"Loaded:%@\n",bundle);
  return NSApplicationMain(argc,argv);
}

Other ideas for testing plugins:

  • use an independent bundle: Do not specify either BUNDLE_LOADER or TEST_HOST and put your classes from the plugin also into the unittest bundle.
  • put your test cases into the plugin, and try to get that unittest. Just weak link SenTestingKit to your plugin and add a script phase with: TEST_RIG=/Developer/Tools/otest "${SYSTEM_DEVELOPER_DIR}/Tools/RunUnitTests".
mfazekas
I liked the idea of not setting the test host and loading the plugin from the unit test bundle, but that results in GDB complaining: "This GDB was configured as "i386-apple-darwin".tty /dev/ttys000Loading program into debugger…sharedlibrary apply-load-rules allrunRunning…No executable file specified.Use the "file" or "exec-file" command."
Elise van Looij
The outside test host approach has worked the best so far, except for the debugger not working, as detailed in the third comment to jib's answer. I tried the weak link / TEST_RIG thing, but could not make it work. It did make my notice the Test Rig build setting, which I'd never seen before, but couldn't make that work either. Well, it did work, but was extremely slow and ended up reporting 218 errors on a single STAssertNil with no breaking into the debugger.
Elise van Looij
With no host for example debugger should like this: add a custom executable as /Developer/Tools/otest then add arguments -SenTest All then full path to your .octest. Then select this new cutsom executable from Project/Set Active Executable. Then Run/Debug. If complains about GC then add an environment variable: OBJC_DISABLE_GC YES to the ocest.
mfazekas