blog

Image of speed limit sign. Photo by everett mcintire on Unsplash

Shared Behavior in Objective-C Unit Testing with Specta

by

Last Spring, Steve Huey posted about Specta, a light-weight TDD / BDD framework for Objective-C.  I’ve recently been working on a project with Steve using Specta to test out Core Data entities, and found its relatively undocumented behavior sharing feature to be useful.

Reusable behavior-based tests allow distinct classes with similar prototypes to share the same test cases without duplicating the test code.  It’s great for testing two or more classes that have a shared protocol, whether formal or informal.  For example:

  • Subclasses of NSManagedObject that all need to test insertion, assigning properties, saving in a context, etc.
  • Data sources that conform to a protocol and all need to test providing data at an index
  • Multiple implementations of a single data structure that all need to test setting and retrieving data by key

Specta provides the methods sharedExamplesFor and itShouldBehaveLike to allow the same set of test cases to be run from multiple specs.

Specta’s shared behavior code

In the following example, three test cases are shared by specs for NSArray and NSOrderedSet. The tests exercise common functionality for any ordered collection:

SharedExamplesBegin(OrderedCollectionBehavior)
// A set of shared test cases are defined here under the key name
// "an ordered collection".
//
// The dictionary param is the only means of providing data from
// the shared examples caller to the set of shared examples.
// In this case pass in the collection class to be tested.
sharedExamplesFor(@"an ordered collection", ^(NSDictionary* data)
{
   // Pull the class to test out of the data object
   Class collectionClass = data[@"collectionClass"];
   it(@"should be empty when first created",
   ^{
      id collection = [[collectionClass alloc] init];
      expect([collection count]).to.equal(0);
   });
   it(@"should allowing initializing with a contained object",
   ^{
      expect(
      ^{
         (void) [[collectionClass alloc] initWithObject:@"foo"];
      }).notTo.raise(nil);
   });
   it(@"should reply to first with the object provided during init",
   ^{
      NSString* object = @"foo";
      id collection = [[collectionClass alloc] initWithObject:object];
      expect([collection firstObject]).to.equal(object);
   });
});
SharedExamplesEnd
// Here's one instance of a spec making use of the shared behavior.
// Test that an array behaves like an ordered collection.
SpecBegin(NSArray)
describe(@"shared array behavior",
^{
   NSDictionary* data = @{@"collectionClass": [NSArray class]};
   itShouldBehaveLike(@"an ordered collection", data);
});
SpecEnd
// And here's a second instance of a spec making use of
// the shared behavior. Test that an ordered set behaves
// like an ordered collection.
SpecBegin(NSOrderedSet)
describe(@"shared ordered set behavior",
^{
   NSDictionary* data = @{@"collectionClass": [NSOrderedSet class]};
   itShouldBehaveLike(@"an ordered collection", data);
});
SpecEnd

The sharing utility methods’ first parameter – the shared behavior name string – must match.  The second parameter is a dictionary for passing any inputs from the caller into the shared behavior to customize it for the particular class under testing.

The sharedExamplesFor block supports all the utility functions available in a non-shared set of test cases, like beforeAll, beforeEach, afterEach, afterAll, describe, etc.  Both the each functions in the shared behavior block and the each functions in the calling spec will be run each time a shared test case is run for the calling spec.

Behavior testing in other frameworks

How common are behavior sharing tools are in other test frameworks?

By itself, OCUnit doesn’t appear to offer any support – you might be stuck with writing macros, or perhaps a category on SenTestCase might work.  Alternatively, this blog post on parameterized test cases might help.

Ruby’s RSpec testing tool supports shared test cases through its shared examples and contexts, which pretty closely mirror Specta’s support.  Does Python’s unittest?

In the JavaScript world, Mocha has shared behaviors, as does Jasmine.  I’m not sure about QUnit, but JavaScript is flexible enough that it probably does.

+ more