Descriptive Test Names
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.