RSpec-like Nested Contexts for XCTest

Posted by on Jan 21, 2014 in Blog | Tags: , , | No Comments

There is a lot to like about the enhancements to the unit testing workflow in Xcode 5, in particular the ability to see in the gutter of a test suite which tests are passing and which are not, and to re-run a single test. Unfortunately, I am also a fan of Kiwi, Specta and their RSpec-like syntax, which doesn’t play well with the test navigator. One of the best ways to organize specs with those tools are nested contexts.

Lately for me, XCTest and its advantages had won out over my preference for RSpec-like syntax. Still, it really bugged me that I couldn’t use nested contexts. It was so convenient to set up some common state, then specialize the state further in beforeEach() blocks throughout the hierarchy. This afternoon, I came up with this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
- (void) setUp
{
    [super setUp];
 
    NSString *testCase = NSStringFromSelector([self selector]);
    if ([testCase hasPrefix:@"test"])
    {
        testCase = [testCase substringFromIndex:4];
        NSArray *contexts = [testCase componentsSeparatedByString:@"_"];
        if ([contexts count] > 1)
        {
            NSUInteger i = 0;
            while (i < [contexts count] - 1)
            {
                NSString *context = contexts[i++];
                SEL selector = NSSelectorFromString([NSString stringWithFormat:@"setUp%@", context]);
                if ([self respondsToSelector:selector])
                {
                    void (*function)(id, SEL) = (void *)[self methodForSelector:selector];
                    function(self, selector);
                }
            }
        }
    }
}
 
- (void) setUpOuterContext
{
    // ...
}
 
- (void) setUpInnerContext
{
    // ...
}
 
- (void) testOuterContext_InnerContext_ThingIAmTesting
{
    // ...
}

XCTest always calls -setUp before each test case. The implementation here looks at the name of the currently running test case. If it contains underscores, each underscore-delimited part is treated as a context and it looks for a matching -setUpXXX method to call.

The test case names get a little messier and certainly more verbose, but it beats adding calls to set up state in every test case. Plus, in an alphabetical list of methods, they will group naturally with other test cases in the same context.

This example is missing -tearDown, but that’s easy enough to implement. Remember to change the prefix on line 16 so each context’s tear-down method is called, if it exists.

It’s not perfect, but it’ll do just fine.