Quinn Taylor
Quinn Taylor

Reputation: 44769

How to implement or emulate an "abstract" OCUnit test class?

I have a number of Objective-C classes organized in an inheritance hierarchy. They all share a common parent which implements all the behaviors shared among the children. Each child class defines a few methods that make it work, and the parent class raises an exception for the methods designed to be implemented/overridden by its children. This effectively makes the parent a pseudo-abstract class (since it's useless on its own) even though Objective-C doesn't explicitly support abstract classes.

The crux of this problem is that I'm unit testing this class hierarchy using OCUnit, and the tests are structured similarly: one test class that exercises the common behavior, with a subclass corresponding to each of the child classes under test. However, running the test cases on the (effectively abstract) parent class is problematic, since the unit tests will fail in spectacular fashion without the key methods. (The alternative of repeating the common tests across 5 test classes is not really an acceptable option.)

The non-ideal solution I've been using is to check (in each test method) whether the instance is the parent test class, and bail out if it is. This leads to repeated code in every test method, a problem that becomes increasingly annoying if one's unit tests are highly granular. In addition, all such tests are still executed and reported as successes, skewing the number of meaningful tests that were actually run.

What I'd prefer is a way to signal to OCUnit "Don't run any tests in this class, only run them in its child classes." To my knowledge, there isn't (yet) a way to do that, something similar to a +(BOOL)isAbstractTest method I can implement/override. Any ideas on a better way to solve this problem with minimal repetition? Does OCUnit have any ability to flag a test class in this way, or is it time to file a Radar?


Edit: Here's a link to the test code in question. Notice the frequent repetition of if (...) return; to start a method, including use of the NonConcreteClass() macro for brevity.

Upvotes: 3

Views: 582

Answers (5)

Rudolf Adamkovič
Rudolf Adamkovič

Reputation: 31486

The simplest way:

- (void)invokeTest {
    [self isMemberOfClass:[AbstractClass class]] ?: [super invokeTest];
}

Copy, paste and replace AbstractClass.

Upvotes: 1

Chris Devereux
Chris Devereux

Reputation: 5473

It sounds like you want a parameterized test.

Parameterized tests are great whenever you want to have a large number of tests with the same logic but different variables. In this case, the parameter to your test would be the concrete tested class, or possibly a block that will create a new instance of it.

There's an article about implementing parameterized testing in OCUnit here. Here's an example of applying it to testing a class hierarchy:

@implementation MyTestCase {
    RPValue*(^_createInstance)(void);
    MyClass *_instance;
}

+ (id)defaultTestSuite
{
    SenTestSuite *testSuite = [[SenTestSuite alloc] initWithName:NSStringFromClass(self)];

    [self suite:testSuite addTestWithBlock:^id{
        return [[MyClass1 alloc] initWithAnArgument:someArgument];
    }];

    [self suite:testSuite addTestWithBlock:^id{
        return [[MyClass2 alloc] initWithAnotherArgument:someOtherArgument];
    }];

    return testSuite;
}

+ (void)suite:(SenTestSuite *)testSuite addTestWithBlock:(id(^)(void))block
{
    for (NSInvocation *testInvocation in [self testInvocations]) {
        [testSuite addTest:[[self alloc] initWithInvocation:testInvocation block:block]];
    }
}

- (id)initWithInvocation:(NSInvocation *)anInvocation block:(id(^)(void))block
{
    self = [super initWithInvocation:anInvocation];
    if (!self)
        return nil;

    _createInstance = block;

    return self;
}

- (void)setUp
{
    _value = _createInstance();
}

- (void)tearDown
{
    _value = nil;
}

Upvotes: 1

Jonathan Arbogast
Jonathan Arbogast

Reputation: 9650

Here's a simple strategy that worked for me. Just override invokeTest in your AbstractTestCase as follows:

- (void) invokeTest {
    BOOL abstractTest = [self isMemberOfClass:[AbstractTestCase class]];
    if(!abstractTest) [super invokeTest];
}

Upvotes: 4

Mikayel Aghasyan
Mikayel Aghasyan

Reputation: 225

You could also override + (id)defaultTestSuite method in your abstract TestCase class.

+ (id)defaultTestSuite {
    if ([self isEqual:[AbstractTestCase class]]) {
        return nil;
    }
    return [super defaultTestSuite];
}

Upvotes: 1

Jon Reid
Jon Reid

Reputation: 20980

I don't see a way to improve on the way you're currently doing things without digging into OCUnit itself, specifically the SenTestCase implementation of -performTest:. You'd be set if it called invoked a method to determine "Should I run this test?" The default implementation would return YES, while your version would be like your if-statement.

I'd file a Radar. The worst that could happen is your code stays the way it is now.

Upvotes: 0

Related Questions