Skip to content
idoru edited this page Oct 4, 2012 · 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 nest contexts so that it is easier and clearer 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 "MyNumberSequencer.h"

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

SPEC_BEGIN(MyNumberSequencerSpec)

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

    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(@"MyNumberSequencerSpec", ^{
    __block MyNumberSequencerSpec * myNumberSequencer;

    it(@"nextAfter: returns the next integer greater than the argument", ^{
        myNumberSequencer = [[MyNumberSequencer 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(@"MyNumberSequencerSpec", ^{
    __block MyNumberSequencerSpec * myNumberSequencer;

    beforeEach(^{
        myNumberSequencer = [[MyNumberSequencer 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);
    });
});

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(@"MyNumberSequencerSpec", ^{
    __block MyNumberSequencerSpec * myNumberSequencer;

    describe(@"when created with the default constructor", ^{
        beforeEach(^{
            myNumberSequencer = [[MyNumberSequencer 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 = [[MyNumberSequencer 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 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 are what allows your test to make assertions like the following:

[myNumberSequencer nextAfter:2] should equal(4);
//OR
expect([myNumberSequencer nextAfter:2]).to(equal(4));

These matchers use C++ templates to circumvent type issues that plague other matcher libraries. For example, rather than this (OCHamcrest):

assertThat(aString, equalTo(@"something"));
assertThatInt(anInteger, equalToInt(7));
assertThatInt(anInteger, isNot(equalToInt(9)));
assertThatBool(aBoolean, equalTo(YES));

you can write the following:

expect(aString).to(equal(@"something"));
expect(anInteger).to(equal(7));
expect(anInteger).to_not(equal(9));
expect(aBoolean).to(equal(YES));

although you would more likely write the last line as:

expect(aBoolean).to(be_truthy());

Here is a list of built-in matchers you can use:

expect(...).to(be_nil());

expect(...).to(be_truthy());
expect(...).to_not(be_truthy());

expect(...).to(equal(10));
expect(...).to == 10; // shortcut to the above
expect(...) == 10; // shortcut to the above

expect(...).to(be_greater_than(5));
expect(...).to > 5; // shortcut to the above
expect(...) > 5; // shortcut to the above

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

expect(...).to(be_less_than(11));
expect(...).to < 11; // shortcut to the above
expect(...) < 11; // shortcut to the above

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

expect(...).to(be_close_to(5)); // default within(.01)
expect(...).to(be_close_to(5).within(.02));

expect(...).to(be_instance_of([NSObject class]));
expect(...).to(be_instance_of([NSObject class]).or_any_subclass());

expect(...).to(be_same_instance_as(object));

expect(...).to(contain(@"something"));
expect(...).to(be_empty());

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

These matchers use C++ templates for type deduction. 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.

Note: If you prefer RSpec's should syntax you can write your expectations as follows:

1 + 2 should equal(3);
glass should_not be_empty();

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.

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]));

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