-
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 to nest contexts so that it is easier 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 "NumberSequencer.h"
using namespace Cedar::Matchers;
using namespace Cedar::Doubles;
SPEC_BEGIN(NumberSequencerSpec)
describe(@"NumberSequencer", ^{
__block NumberSequencer *model;
beforeEach(^{
});
});
SPEC_END
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);
});
});
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);
});
});
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.
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.
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.
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.
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. 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 existing object
spy_on(someInstance);
// class fakes
id<CedarDouble> fake = fake_for(someClass);
id<CedarDouble> niceFake = nice_fake_for(someClass);
// protocol fakes
id<CedarDouble> anotherFake = fake_for(someProtocol);
id<CedarDouble> anotherNiceFake = nice_fake_for(someProtocol);
A fake responds to stubbed methods and records their invocations. If a fake receives a message for a method that hasn't been stubbed, then it will either return 0
/nil
/NULL
(if the fake is nice; it will also still record the invocation) or raise an exception (if the fake is not nice). When asked if it responds to an optional protocol method, a non-nice protocol fake returns YES
only if the method has been stubbed.
You can stub methods on doubles so that they do nothing:
//stubbing all calls to method:; "method:" can be used instead of @selector("method:") for brevity
fake stub_method(@selector("method:"));
fake stub_method("method:");
//only stubbing calls with specific arguments
fake stub_method("method:").with(x);
//methods with multiple arguments; both forms below are equivalent
fake stub_method("method:withSecondArg:").with(x).and_with(y);
fake stub_method("method:withSecondArg:").with(x, y);
//matching an arbitrary argument
fake stub_method("method:withSecondArg:").with(x, Arguments::anything);
//matching an arbitrary instance of a specific class
fake stub_method("method:withSecondArg:").with(x, Arguments::any([NSArray class]));
Or return a canned value:
fake stub_method("method:").and_return(z);
fake stub_method("method:").with(x).and_return(z);
Or execute an alternative implementation provided by your test:
fake stub_method("method").and_do(^(NSInvocation * invocation) {
//do something different here
});
Or raise an exception:
fake stub_method("method").and_raise_exception();
fake stub_method("method").and_raise_exception([NSException]);
There is also a have_received
matcher so you can assert that doubles have received particular messages. For example, if you had spied upon an object someInstance
and it had received the message someMethod:
with x
:
someInstance should have_received("someMethod:").with(x);
someInstance should_not have_received("someMethod:").with(y);
More examples:
//both forms below are equivalent
someInstance should have_received("someMethod:withSecondArg:").with(x, y);
someInstance should have_received("someMethod:withSecondArg:").with(x).and_with(y);
someInstance should have_received("someMethod:").with(Arguments::anything);
someInstance should have_received("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 an array of NSInvocation objects):
NSArray *messages = [(id<CedarDouble>)spy sent_messages];
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.