Let's say I'm writing an RSS reader. (With apologies to Brent Simmons.)

And let's say that it will support a number of different services. In order to make it easier to write different implementations for each service, I might write Feed and Folder protocols to allow them to share a common interface. The Folder protocol might look something like:

@protocol Folder <NSObject>

@property (readonly, strong) NSArray<id<Feed>> *feeds;
- (void)addFeeds:(NSArray<id<Feed>> *)feeds;
/**/

@end

With this protocol, I can implement concrete classes for each service, such as LocalFolder, FeedBinFolder, FeedlyFolder, etc.

But how do I test them?

Being Folder-compliant, all instances of these classes must maintain a common set of guarantees. For example, adding feeds with addFeeds: should cause those feeds to be accessible via the feeds property of the folder. That test could look like:

- (void)testAddFeed {
    id<Folder> testFolder = [self folder];
    id<Feed> testFeed = [self feed];
    [testFolder addFeeds:@[testFeed]];
    XCTAssert([testFolder.feeds containsObject:testFeed]);
}

I'd like to run this test against all the different types that conform to Folder, and this is made possible by test subclassing.

Here's a test superclass, filled with the tests I'd like to run on all my Folder-compliant types, configured by default to test LocalFolder:

@implementation FolderTests

- (id<Folder>)folder {
    return [LocalFolder new];
}

- (id<Feed>)feed {
    return [LocalFeed new];
}

- (void)testAddFeed {
    id<Folder> testFolder = [self folder];
    id<Feed> testFeed = [self feed];
    [testFolder addFeeds:@[testFeed]];
    XCTAssert([testFolder.feeds containsObject:testFeed]);
}

- (void)testAddThousandsOfFeeds {
    [self measureBlock:^{
        /**/
    }];
}

@end

The interface describes the methods that subclasses should overload:

@interface FolderTests : XCTestCase

- (id<Folder>)folder;
- (id<Feed>)feed;

@end

To test the other Folder-compliant types, I implement subclasses of FolderTests like this one, for FeedBinFolder:

@interface FeedBinFolderTests : FolderTests
@end

@implementation FeedBinFolderTests

- (id<Folder>)folder {
    return [FeedBinFolder new];
}

- (id<Feed>)feed {
    return [FeedBinFeed new];
}

- (void)testFeedBinSpecific {
    /**/
}

@end

At this point, I've written a total of three test methods. Running the test target, the console shows:

Test Suite 'All tests' started at 2015-07-31 11:30:55.855
Test Suite 'FolderTests.xctest' started at 2015-07-31 11:30:55.857
Test Suite 'FeedBinFolderTests' started at 2015-07-31 11:30:55.857
Test Case '-[FeedBinFolderTests testAddFeed]' started.
Test Case '-[FeedBinFolderTests testAddFeed]' passed (0.000 seconds).
Test Case '-[FeedBinFolderTests testAddThousandsOfFeeds]' started.
Test Case '-[FeedBinFolderTests testAddThousandsOfFeeds]' measured [Time, seconds] <…snip…>
Test Case '-[FeedBinFolderTests testAddThousandsOfFeeds]' passed (0.492 seconds).
Test Case '-[FeedBinFolderTests testFeedBinSpecific]' started.
Test Case '-[FeedBinFolderTests testFeedBinSpecific]' passed (0.000 seconds).
Test Suite 'FeedBinFolderTests' passed at 2015-07-31 11:30:56.351.
     Executed 3 tests, with 0 failures (0 unexpected) in 0.492 (0.494) seconds
Test Suite 'FolderTests' started at 2015-07-31 11:30:56.351
Test Case '-[FolderTests testAddFeed]' started.
Test Case '-[FolderTests testAddFeed]' passed (0.000 seconds).
Test Case '-[FolderTests testAddThousandsOfFeeds]' started.
Test Case '-[FolderTests testAddThousandsOfFeeds]' measured [Time, seconds]  <…snip…>
Test Case '-[FolderTests testAddThousandsOfFeeds]' passed (0.259 seconds).
Test Suite 'FolderTests' passed at 2015-07-31 11:30:56.611.
     Executed 2 tests, with 0 failures (0 unexpected) in 0.259 (0.260) seconds
Test Suite 'FolderTests.xctest' passed at 2015-07-31 11:30:56.611.
     Executed 5 tests, with 0 failures (0 unexpected) in 0.752 (0.755) seconds
Test Suite 'All tests' passed at 2015-07-31 11:30:56.612.
     Executed 5 tests, with 0 failures (0 unexpected) in 0.752 (0.757) seconds

I wrote three test methods, but XCTest ran five tests. If I wrote the most basic FolderTests subclass possible for FeedlyFolder, the effective test count would go from five to seven. If I then added a testRemoveFeeds test to FolderTests, it would be inherited by the test subclasses as well, raising the count further to ten.

This approach takes advantage of a powerful yet little known feature of XCTest; creating a test case subclass causes that class to inherit and run all of its ancestors' tests. This is really useful pattern for testing functional protocol conformances, such as our working example, or high level black box testing for classes that support configuration, like a database engine or compiler.


The example code behind the snippets in this post can be found here.