I was reading this blog post about the state of Ruby testing, and it introduced some nifty syntax for writing unit tests in Ruby. Instead of using method names as the test name:

def test_separate_invalids_creates_invalid
  nurse = Nurse.new
  nurse.separate_invalids!

  assert_create_invalid_file_for "Job"
  assert_create_invalid_file_for "JobReport"
  assert_create_invalid_file_for "JobView"
end

You could give each test a string name, like this:

test "separate_invalids! creates invalid file for each model" do
  nurse = Nurse.new
  nurse.separate_invalids!
  assert_create_invalid_file_for "Job"
  assert_create_invalid_file_for "JobReport"
  assert_create_invalid_file_for "JobView"
end

I wondered if we could do something like this in Objective-C with OCUnit. It turns out it’s possible with some preprocessor magic.

Say, we’ve got this test class:

@implementation SomeTest

- (void)testSomeLongDescription
{
    STFail(@"failed", nil);
}

- (void)testAnotherLongDescription
{
    STFail(@"failed", nil);
}

@end

When these fail, you get a log messages like these in the Xcode console:

error: -[SomeTest testSomeLongDescription] : failed
error: -[SomeTest testAnotherLongDescription] : failed

I created some macros that allow you to write tests like this instead:

@implementation SomeTest

DD_TEST(@"some long description")
{
    STFail(@"failed", nil);
}

DD_TEST(@"another long description")
{
    STFail(@"failed", nil);
}

@end

Now when these fail, you get error messages like this:

error: -[SomeTest 'some long description'] : failed
error: -[SomeTest 'another long description'] : failed

While I was able to get this to work, there’s one fatal flaw: Xcode’s function popup no longer shows anything useful. Instead of a list of different method names:

We now get a list of functions, all with the same name:

This is makes the function popup effectively worthless, and is unfortunately a deal breaker for me. I’ve tried a few tricks, and I can’t figure out any way to get the popup to show anything useful. If anyone has any ideas, I’d love to hear about them. Otherwise, I’m going to file this away under “fun, but useless hacks”.

Implementation

For the curious, here’s how I implemented the macros:

#define DD_TEST(_NAME_) DD_TEST2(_NAME_, __LINE__)

#define DD_TEST2(_NAME_, _LINE_) DD_TEST3(_NAME_, _LINE_)

#define DD_TEST3(_NAME_, _LINE_) \
+ (NSString *)testLine_ ## _LINE_ { return _NAME_; } \
- (void)testLine_ ## _LINE_

This creates a unique test method name based on the current line number. It then creates a class method with the same name that returns the test’s descriptive name. You have to use a double macro assignment to get __LINE__ to expand properly. Here’s an example of how they would expand:

+ (NSString *)testLine_50 { return @"some long description"; }
- (void)testLine_50
{
    STFail(@"failed", nil);
}

To get OCUnit to use this descriptive name requires that we override the -name method in the test class as such:

- (NSString *)name
{
    SEL selector = [[self invocation] selector];
    if ([[self class] respondsToSelector:selector])
    {
        NSString * testName = [[self class] performSelector:selector];
        return [NSString stringWithFormat:@"-[%@ '%@']",
                [self className], testName];
    }
    else
        return [super name];
}

The invocation property is an NSInvocation representing the current running test method. We grab the selector and look for a class method of the same name. If we find one, we call it and use the return value as the test name.

UPDATE 4-Dec-2008: Dave Ewing replied on Twitter that in C++ mode, the function popup behaves differently. And indeed it does, in a very good way:

The only downside is that refactoring does not work in Objective-C++ mode. As I’ve come to rely on refactoring, this still isn’t the perfect solution. “Edit All in Scope” still works, though. Perhaps I’ll try hacking the C.xclangspec file, if I get bored, to see if I can get straight Objective-C mode to recognize functions like that.

I also corrected the DD_TEST macros, as you really need two levels of indirection for the __LINE__ macro to expand in another macro. Yeah, it’s weird preprocessor voodoo.