Skip to content
Alex Basson edited this page Jul 19, 2013 · 21 revisions

Writing specs is very similar to writing OCUnit tests, except that the language is biased towards describing the behavior of your objects. Cedar specs also allow you to nest contexts so that it is easier to understand how your object behaves in different scenarios.

Creating a new spec file

  1. Create a new file in your spec target.
  2. Select the "Cedar Spec" template appropriate for the platform your tests will run on.
  3. Enter the name of the class that you are writing the spec for.

Structuring your specs

Your newly created spec should look a little like this:

#import "NumberSequencer.h"

using namespace Cedar::Matchers;
using namespace Cedar::Doubles;

SPEC_BEGIN(NumberSequencerSpec)

describe(@"NumberSequencer", ^{
    __block NumberSequencer *model;

    beforeEach(^{

    });
});

SPEC_END

Adding tests (Examples)

You can add a single test (we call these examples) by placing an it block within the describe block:

describe(@"NumberSequencer", ^{
    __block NumberSequencer *myNumberSequencer;

    it(@"nextAfter: returns the next integer greater than the argument", ^{
        myNumberSequencer = [[NumberSequencer alloc] init];
        [myNumberSequencer nextAfter:2] should equal(3);
    });
});

Extracting common setup - beforeEach

If you have setup which is common to many tests, put this in a beforeEach block to save duplication and reduce individual tests to their salient details. beforeEach blocks run before each test.

describe(@"NumberSequencer", ^{
    __block NumberSequencer *myNumberSequencer;

    beforeEach(^{
        myNumberSequencer = [[NumberSequencer alloc] init];
    });

    it(@"nextAfter: returns the smallest integer greater than the argument", ^{
        [myNumberSequencer nextAfter:2] should equal(3);
    });

    it(@"previousBefore: returns the largest integer less than the argument", ^{
        [myNumberSequencer previousBefore:2] should equal(1);
    });
});

Extracting common setup - subjectAction blocks

Generally you want each top-level describe block to describe a single method or action. Often you end up calling this action in multiple places at multiple levels of nesting after various amounts of setup. In this case you can use a subjectAction block to simply your specs. A subjectAction block differs from a beforeEach block because you may have only one for any given example group (if multiple levels define a subjectAction block Cedar will throw an exception), and it will run after all beforeEach blocks for a given example. For example:

describe(@"thing", ^{
    __block BOOL parameter;

    subjectAction(^{ [object doThingWithParameter:parameter]; });

    describe(@"when something is true", ^{
        beforeEach(^{
            parameter = YES;
        });

        it(@"should ...", ^{
            // ...
        });
    });
});

In this case the parameter will be set to YES before the subjectAction runs.

Isolating behaviors in different scenarios

If your class behaves differently in different contexts, you can nest describe blocks with different beforeEach blocks. You can also use context blocks, which is an alias for describe which often conveys intent more clearly:

describe(@"NumberSequencer", ^{
    __block NumberSequencer *myNumberSequencer;

    describe(@"when created with the default constructor", ^{
        beforeEach(^{
            myNumberSequencer = [[NumberSequencer alloc] init];
        });

        it(@"nextAfter: returns the smallest integer greater than the argument", ^{
            [myNumberSequencer nextAfter:2] should equal(3);
        });

        it(@"previousBefore: returns the largest integer less than the argument", ^{
            [myNumberSequencer previousBefore:2] should equal(1);
        });
    });

    context(@"when constructed with an interval", ^{
        beforeEach(^{
            myNumberSequencer = [[NumberSequencer alloc] initWithInterval:2];
        });

        it(@"nextAfter: returns the sum of the argument and the interval", ^{
            [myNumberSequencer nextAfter:2] should equal(4);
        });

        it(@"previousBefore: returns the difference between the argument and the interval", ^{
            [myNumberSequencer previousBefore:2] should equal(0);
        });
    });
});

When nesting describe/context blocks, the beforeEach blocks for all the containing contexts of an it block are run (from outermost to innermost) before the it block is executed.

You can also provide an afterEach block which is run after each it block. This is often useful for cleaning up resources or resetting global state. When nested, these are run from the innermost to outermost.

Global beforeEach and afterEach

In many cases you have some housekeeping you'd like to take care of before every spec in your entire suite. For example, loading fixtures or resetting a global variable. Cedar will look for the +beforeEach and +afterEach class methods on every class it loads; you can add this class method onto any class you compile into your specs and Cedar will run it. This allows spec libraries and helpers to provide global +beforeEach and +afterEach methods specific to their own functionality, and they will run automatically.

If you want to run your own code before or after every spec, simply declare a class and implement the +beforeEach and/or +afterEach methods.

Matchers

Cedar's matchers allow you to perform assertions in your tests with two different flavors of syntax, depending on your style preferences. Many developers are accustomed to writing assertions as expectations in the spirit of behavior-driven development; Cedar has a rich DSL of expectations which can be expressed with a concise should syntax which should be comfortable for developers coming from RSpec. For example:

aString should equal(@"something");
anInteger should equal(7);
anIntegerValueObject should_not equal(9);
myCollection should_not contain(thisThing);
aBoolean should equal(YES);

Although more idiomatically, you would write the last line as:

aBoolean should be_truthy();

In many cases function call parentheses may be omitted when using the "should" syntax when there is no argument to pass:

aBoolean should be_truthy;
collection should be_empty;

The matchers also support automatic boxing/unboxing of primitive types:

@42 should equal(42);
63 should equal(@63);

If you prefer to express your expectations in a more functional-style, you can also use expect to syntax:

// preferred "should" syntax
[myNumberSequencer nextAfter:2] should equal(4);

// can also be expressed as:
expect([myNumberSequencer nextAfter:2]).to(equal(4));

Here is a list of built-in matchers you can use. Note that all of these can be used with the expect to syntax in addition to should as written below:

... should be_nil;

... should be_truthy;
... should_not be_truthy;

... should equal(10);
expect(...) == 10;

... should be_greater_than(5);
expect(...) > 5;

... should be_greater_than_or_equal_to(10);
... should be_gte(10); // shortcut to the above
expect(...) >= 10;

... should be_less_than(11);
expect(...) < 11;

... should be_less_than_or_equal_to(10);
... should be_lte(10); //shortcut to the above
expect(...) <= 10;

... should be_close_to(5); // default within(.01)
... should be_close_to(5).within(.02);

... should be_instance_of([NSObject class]);
... should be_instance_of([NSObject class]).or_any_subclass();

... should be_same_instance_as(object);

... should contain(@"something");
... should be_empty;

^{ ... } should raise_exception([NSInternalInconsistencyException class]);

These matchers use C++ templates for type deduction, which requires that your spec files be Objective-C++ and use the Cedar::Matchers namespace. If you're migrating from an older version of Cedar, you should:

  • Change the file extension for each of your spec files from .m to .mm (this will tell the compiler that the file contains C++ code).
  • Add the following line to the top of your spec files, after the file includes:
using namespace Cedar::Matchers;

You can also add your own matchers without modifying the Cedar library. Check the Configuration page for details.

These matchers will break Apple's GCC compiler, and versions 2.0 and older of the LLVM compiler (this translates to any compiler shipped with a version of Xcode before 4.1). Fortunately, LLVM 2.1 fixes the issues.

Note: If you decide to use another matcher library that uses expect(...) to build its expectations (e.g. Expecta) you will need to add #define CEDAR_MATCHERS_COMPATIBILITY_MODE before SpecHelper.h is imported (usually in the spec target's pre-compiled header, or at the top of each spec file). That will prevent Cedar from defining a macro that overrides that library's expect function.

Shared example groups

Cedar supports shared example groups; you can declare them in one of two ways: either inline with your spec declarations, or separately.

Declaring shared examples inline with your specs is the simplest:

SPEC_BEGIN(FooSpecs)

sharedExamplesFor(@"a similarly-behaving thing", ^(NSDictionary *context) {
    it(@"should do something common", ^{
        //...
    });
});

describe(@"Something that shares behavior", ^{
    itShouldBehaveLike(@"a similarly-behaving thing");
});

describe(@"Something else that shares behavior", ^{
    itShouldBehaveLike(@"a similarly-behaving thing");
});

SPEC_END

Sometimes you'll want to put shared examples in a separate file so you can use them in several specs across different files. You can do this using macros specifically for declaring shared example groups:

SHARED_EXAMPLE_GROUPS_BEGIN(GloballyCommon)

sharedExamplesFor(@"a thing with globally common behavior", ^(NSDictionary *context) {
    it(@"should do something really common", ^{
        //...
    });
});

SHARED_EXAMPLE_GROUPS_END

The context dictionary allows you to pass example-specific state into the shared example group. You can populate the context dictionary available on the SpecHelper object, and each shared example group will receive it:

sharedExamplesFor(@"a red thing", ^(NSDictionary *context) {
    it(@"should be red", ^{
        Thing *thing = [context objectForKey:@"thing"];
        expect(thing.color).to(equal(red));
    });
});

describe(@"A fire truck", ^{
    beforeEach(^{
        [[SpecHelper specHelper].sharedExampleContext setObject:[FireTruck fireTruck] forKey:@"thing"];
    });
    itShouldBehaveLike(@"a red thing");
});

describe(@"An apple", ^{
    beforeEach(^{
        [[SpecHelper specHelper].sharedExampleContext setObject:[Apple apple] forKey:@"thing"];
    });
    itShouldBehaveLike(@"a red thing");
});

Doubles

Doubles provide a way for you record the messages sent to an object so that you can verify them later. You can also create a fake of a class or protocol and use it in place of a real object. This makes them extremely useful in isolating your object under test. If you want to learn how test doubles can be used a good place to start is Martin Fowler's seminal Mocks Aren't Stubs.

spy_on(someInstance);
id<CedarDouble> fake = fake_for(someClass);
id<CedarDouble> anotherFake = fake_for(someProtocol);
id<CedarDouble> niceFake = nice_fake_for(someClass);
id<CedarDouble> anotherNiceFake = nice_fake_for(someProtocol);

You can also stub methods on doubles so that they do nothing:

fake stub_method("selector").with(x);
fake stub_method("selector").with(anything);
fake stub_method("selector").with(x).and_with(y);

Or return a canned value:

fake stub_method("selector").and_return(z);
fake stub_method("selector").with(x).and_return(z);

Or execute an alternative implementation provided by your test:

fake stub_method("selector").and_do(^(NSInvocation * invocation) {
    //do something different here
});

Or raise an exception:

fake stub_method("selector").and_raise_exception();
fake stub_method("selector").and_raise_exception([NSException]);

There are also matchers available for doubles to verify messages received:

someInstance should have_received(@selector(someMethod:)).with(x);
someInstance should have_received("someMethod:").with(x);

someInstance should have_received(@selector(someMethod:withSecondArg:)).with(x).and_with(y);

someInstance should have_received(@selector(someMethod:)).with(Arguments::anything);
someInstance should have_received(@selector(someMethod:)).with(Arguments::any([NSString class]));

If you need to, you can also reset the messages captured by a double:

[(id<CedarDouble>)spy reset_sent_messages];

You can also access the list of captured messages (as NSInvocation objects):

NSArray *messages = [(id<CedarDouble>)spy sent_messages];

Pending specs

If you'd like to specify but not implement an example you can do so like this:

it(@"should do something eventually", PENDING);

The spec runner will not try to run this example, but report it as pending. The PENDING keyword simply references a nil block pointer; if you prefer you can explicitly pass nil as the second parameter. The parameter is necessary because C, and thus Objective-C, doesn't support function parameter overloading or default parameters.

Clone this wiki locally