-
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.
- Select the language you prefer, Swift or Objective-C. If you don't see this you might want to re-install cedar file templates.
- Enter the name of the class that you are writing the spec for.
Your newly created spec should look a little like this in Objective-C:
#import "NumberSequencer.h"
using namespace Cedar::Matchers;
using namespace Cedar::Doubles;
SPEC_BEGIN(NumberSequencerSpec)
describe(@"NumberSequencer", ^{
__block NumberSequencer *model;
beforeEach(^{
});
});
SPEC_END
Swift looks a little bit different. Since Swift deals with modules rather than individual files as dependencies it requires a @testable import
of the module containing the code you want to test (typically, it's your app). You'll probably also want a matcher library (like Nimble) since Cedar matchers don't work with Swift:
import Cedar
import Nimble
@testable import MyApp
class NumberSequencerSpec: CDRSpec {
override func declareBehaviors() {
var sequencer: NumberSequencer!
beforeEach {
}
}
}
You can add a single test (we call these examples) within an it
block. Put this within the describe
block in Objective-C:
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);
});
});
Or within the body of declareBehaviors
for examples at the top-level of a Swift spec:
class NumberSequencerSpec: CDRSpec {
override func declareBehaviors() {
var sequencer: NumberSequencer!
it("nextAfter: returns the next integer greater than the argument") {
sequencer = NumberSequencer()
expect(sequencer.nextAfter(2)).to(equal(3))
}
}
}
If you have setup which is common to many tests, put this in a beforeEach
block to save duplication and clearly separate the scenario from the expected behavior. beforeEach
blocks run before each test.
Objective-C:
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);
});
});
Swift:
class NumberSequencerSpec: CDRSpec {
override func declareBehaviors() {
var sequencer: NumberSequencer!
beforeEach {
sequencer = NumberSequencer()
}
it("nextAfter: returns the next integer greater than the argument") {
expect(sequencer.nextAfter(2)).to(equal(3))
}
it("previousBefore: returns the largest integer less than the argument") {
expect(sequencer.previousBefore(2)).to(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 simplify 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, in Objective-C:
describe(@"thing", ^{
__block BOOL parameter;
subjectAction(^{ [object doThingWithParameter:parameter]; });
describe(@"when something is true", ^{
beforeEach(^{
parameter = YES;
});
it(@"should ...", ^{
// ...
});
});
});
Swift:
class ThingSpec: CDRSpec {
override func declareBehaviors() {
var parameter: Bool!
var object: Thing!
beforeEach { object = Thing() }
subjectAction { object.doThingWithParameter(parameter) }
describe("when the parameter is true") {
beforeEach { parameter = true }
it("does something") { /* ... */ }
}
describe("when the parameter is false") {
beforeEach { parameter = false }
it("does something different") { /* ... */ }
}
}
}
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
, however the wording can often conveys intent more clearly when used thoughtfully with the domain you're specifying behaviors in.
Objective-C nesting example:
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);
});
});
});
Swift nesting example:
class NumberSequencerSpec: CDRSpec {
override func declareBehaviors() {
var myNumberSequencer: NumberSequencer!
describe("when created with the default constructor") {
beforeEach { myNumberSequencer = NumberSequencer() }
it("nextAfter: returns the smallest integer greater than the argument") {
expect(myNumberSequencer.nextAfter(2)).to(equal(3))
}
it("previousBefore: returns the largest integer less than the argument") {
expect(myNumberSequencer.previousBefore(2)).to(equal(1))
}
}
context("when constructed with an interval") {
beforeEach { myNumberSequencer = NumberSequencer(interval:2) }
it("nextAfter: returns the sum of the argument and the interval") {
expect(myNumberSequencer.nextAfter(2)).to(equal(4))
}
it("previousBefore: returns the difference between the argument and the interval") {
expect(myNumberSequencer.previousBefore(2)).to(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.
Note that Cedar's matchers aren't available in Swift tests as they are implemented in Objective-C++. We use Cedar with the venerable Nimble matcher library to get effectively similar functionality, you should check it out if you're using Swift.
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.
using namespace Cedar::Matchers;
You can easily declare your own matchers with this convenient block syntax:
// Defined in your spec, or in a test helper somewhere:
CedarBlockMatcher be_a_palindrome = matcher(^(NSString *subject) {
subject = [subject lowercaseString];
return [subject isEqual:[subject reversedString]]; // -reversedString defined elsewhere
}).with_failure_message_end(@"be a palindrome")();
it(@"should match palindromes", ^{
@"Anna" should be_a_palindrome; // passes
@"Tom" should be_a_palindrome; // fails with: Expected <Tom> to be a palindrome
});
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. In Objective-C:
SPEC_BEGIN(FooSpecs)
sharedExamplesFor(@"a similarly-behaving thing", ^(NSDictionary *sharedContext) {
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
And Swift:
class FooSpecs: CDRSpec {
override func declareBehaviors() {
sharedExamplesFor("a similarly-behaving thing") { context in
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")
}
}
}
Sometimes you'll want to put shared examples in a separate file so you can use them in several specs across different files. In Objective-C, 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 *sharedContext) {
it(@"should do something really common", ^{
//...
});
});
SHARED_EXAMPLE_GROUPS_END
In Swift, you just making a subclass of a special container for these, a CDRSharedExampleGroupPool
:
class GloballyCommonSharedSpecs: CDRSharedExampleGroupPool {
override func declareSharedExampleGroups() {
sharedExamplesFor("a thing with globally common behavior") { context in
it("should do something really common") {
//...
}
}
}
}
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. For example, in Objective-C:
sharedExamplesFor(@"a red thing", ^(NSDictionary *sharedContext) {
it(@"should be red", ^{
Thing *thing = [sharedContext 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");
});
Swift:
sharedExamplesFor("a red thing") { context in
it("should be red") {
let thing = context["thing"] as! Thing
expect(thing.color).to(equal(red))
}
}
describe("A fire truck") {
beforeEach {
CDRSpecHelper.specHelper().sharedExampleContext["thing"] = FireTruck()
}
itShouldBehaveLike("a red thing")
}
describe("An apple") {
beforeEach {
CDRSpecHelper.specHelper().sharedExampleContext["thing"] = Apple()
}
itShouldBehaveLike("a red thing")
}
You can also pass values to the context with this slightly more terse syntax. Objective-C:
describe(@"An apple", ^{
itShouldBehaveLike(@"a red thing", ^(NSMutableDictionary *context) {
context[@"thing"] = [Apple apple];
});
});
Swift:
describe("A fire truck") {
itShouldBehaveLike("a red thing") { context in
context["thing"] = FireTruck()
}
}
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.
Note that Cedar doubles make use of C++ and extensive dynamism offered by the Objective-C runtime. These features are simply not available on Swift. This means you'll have to rely on creating hand built doubles, although you can get some help generating them.
// 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(@protocol(someProtocol));
id<CedarDouble> anotherNiceFake = nice_fake_for(@protocol(someProtocol));
A spied upon object behaves just like it normally would, until you stub methods on it. In other words, if a spy receives a message for a method that hasn't been stubbed, it will call the real method. This is similar to the functionality OCMock offers with partial mocks.
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];
Or similarly access the list of captured messages filtered by selector name:
NSArray *someMethodMessages = [(id<CedarDouble>)spy sent_messages_with_selector:@selector(someMethod:)];
If you'd like to specify but not implement an example you can do so like this:
Objective-C:
it(@"should do something eventually", PENDING);
Swift:
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.