-
Notifications
You must be signed in to change notification settings - Fork 140
Writing specs
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.
- Create a new file in your spec target.
- Select the "Cedar Spec" template appropriate for the platform your tests will run on.
- Enter the name of the class that you are writing the spec for.
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
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);
});
});
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);
});
});
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.
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.
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();
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 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]));
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.