Cuckoo for Cocoa Dev

A blog for all those Cuckoo for Cocoa Development

Behavioral Driven Development (BDD) Using Cedar Part 2 - Easy Stuff

Since we covered the easier stuff in the previous post, I will jump right using testing techniques that are a tad bit more useful. For this blog post, let’s focus on mock objects (or in Cedar’s case specifically, nice fakes), and very basic dependency injection.

For this demonstration, lets use a “house” model that will be developed using TDD. We will start by writing a failing test.

Note: Since there will be quite a few tests that will be written, I will be displaying the code only and not the output of the tests like in the previous post.

HouseSpec.mm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#import "House.h"

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

SPEC_BEGIN(HouseSpec)

describe(@"House", ^{
    __block House *house;

    beforeEach(^{
        house = [[House alloc] init];
    });

    it(@"exists", ^{
        house should_not be_nil;
    });
});

SPEC_END

Since this won’t compile (I consider this the first failure), let’s fix this:

HouseSpec.h
1
2
@interface House : NSObject
@end
House.m
1
2
3
4
#import "house.h"

@implementation House
@end

Since a empty house isn’t particularly comfortable, let’s add an oven. To make things easier to test, we will use what is called “Constructor Injection.” Rather than creating an instance of an oven object inside the class we are testing, let’s pass the dependency into the class using the “init” method. How will this make our testing lives easier? It will allow us to mock (or fake) our dependencies so that we will focus our house specs on the actual house and not any of it’s appliances or goods. Let us begin:

HouseSpec.mm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#import "House.h"
#import "Oven.h"

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

SPEC_BEGIN(HouseSpec)

describe(@"House", ^{
    __block House *house;
    __block Oven *oven;

    beforeEach(^{
        oven = [[Oven alloc] init];
        house = [[House alloc] initWithOven:oven];
    });

    it(@"exists", ^{
        house should_not be_nil;
    });
});

SPEC_END

Since we haven’t created an oven class, and the -initWithOven: method doesn’t exist, the code will not even compile.

One of the problems with dealing with compiled languages for testing is that if we have any build failures we can’t run the tests. Since the house object doesn’t have a -initWithOven method, and the oven object doesn’t currently exist, the HouseSpec.mm won’t build and we can’t run the oven spec. In situations like this I tend to comment this code out in the other spec (in this case the house) until I can come back to it. This way we can focus on fixing the compile time error for the spec we are currently looking at the moment (oven).

To remedy our compile time Oven woes, we will create an oven spec file and build the oven class:

OvenSpec.mm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#import "Oven.h"

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

SPEC_BEGIN(OvenSpec)

describe(@"Oven", ^{
    __block Oven *oven;

    beforeEach(^{
        oven = [[Oven alloc] init];
    });

    it(@"exists", ^{
        oven should_not be_nil;
    });
});

SPEC_END
Oven.h
1
2
@interface Oven : NSObject
@end
Oven.m
1
2
3
4
#import "Oven.h"

@implementation Oven
@end

Now that we haven taken care of the oven class, let’s continue with the house spec. To get rid of the compile errors, let’s implement the -initWithOven method:

HouseSpec.h
1
2
3
4
5
6
7
#import "Oven.h"

@interface House : NSObject

- (instancetype)initWithOven:(Oven *)oven;

@end
House.m
1
2
3
4
5
6
7
8
9
10
#import "House.h"

@implementation House

- (instancetype)initWithOven:(Oven *)oven
{
    return [super init];
}

@end

When we run the tests, everything is all good.

Usually when an oven is hooked up in a house, it tends to stay there, let’s write a test to check the existence of the oven inside the house:

HouseSpec.mm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#import "House.h"
#import "Oven.h"

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

SPEC_BEGIN(House)

describe(@"House", ^{
    __block House *house;
    __block Oven *oven;

    beforeEach(^{
        oven = [[Oven alloc] init];
        house = [[House alloc] initWithOven:oven];
    });

    it(@"exists", ^{
        house should_not be_nil;
    });

    it(@"has an oven", ^{
        house.oven should_not be_nil;
    });
});

SPEC_END

This will fail obviously, because a house doesn’t currently have an oven property that gets set by the -initWithOven: method. Let’s fix that:

HouseSpec.h
1
2
3
4
5
6
7
8
9
#import "Oven.h"

@interface House : NSObject

@property (strong, nonatomic, readonly) Oven *oven;

- (instancetype)initWithOven:(Oven *)oven;

@end
House.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#import "House.h"

@interface House ()

@property (strong, nonatomic, readwrite) Oven *oven;

@end

@implementation House

- (instancetype)initWithOven:(Oven *)oven
{
    self = [super init];
    if (self) {
        self.oven = oven;
    }
    return self;
}

@end

As you can see, after adding an oven property, we can now store the oven that is injected into the -initWithOven method. For this example I’ve made the oven property read only in the header file (House.h). Since it is read only publicly, to make it writable in the implementation file (House.m) a category needs to be created and the property redeclared as readwrite. With this setup, the oven property is read only by a consumer of the House class and writable internally by the House implementation itself.

It could have been made readwrite, but for this example, the oven property will only be writable in the implementation file. You can even go a step further and make the property “private” by adding a House category in House.m only. Just be sure to add the same category to the spec file so it knows it exists. After adding the property, we are all green again.

Ok, everything is good so far, but let’s make the house a little bit more useful. Since it’s a new house, how about remote appliance power up/down? Let’s do it:

HouseSpec.mm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#import "House.h"
#import "Oven.h"

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


SPEC_BEGIN(HouseSpec)

describe(@"House", ^{
    __block House *house;
    __block Oven *oven;

    beforeEach(^{
        oven = nice_fake_for([Oven class]);
        house = [[House alloc] initWithOven:oven];
    });

    it(@"exists", ^{
        house should_not be_nil;
    });

    it(@"has an oven", ^{
        house.oven should_not be_nil;
    });

    describe(@"Remote appliance power management", ^{
        beforeEach(^{
            [house powerUpAppliances];
        });

        it(@"turns on the oven", ^{
            house.oven should have_received(@selector(powerUp));
        });
    });
});

SPEC_END

Woah, a lot of stuff going on here. Before we fix the failing test, let’s look at the oven object. Since created the -initWithOven: method, the oven can be injected into the object that is under test. Since we don’t really care about the oven object but rather the house object itself, let’s create a mock object that will stand in place of it (or in Cedar’s case a fake). What is nice about the fake is that it is essentially a proxy object, and it stores methods that it has received in the test. If you look at the test, rather than figuring out that the -powerUp method was called on the oven object using side effects (which we don’t have at the moment anyways), we can just ask the fake if it received the power up message.

To fix this, let’s first go back to the oven class and give it the ability to power up. Let’s add a failing test to OvenSpec:

OvenSpec.mm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#import "Oven.h"

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

SPEC_BEGIN(OvenSpec)

describe(@"Oven", ^{
    __block Oven *oven;

    beforeEach(^{
        oven = [[Oven alloc] init];
    });

    it(@"exists", ^{
        oven should_not be_nil;
    });

    it(@"has powerUp method", ^{
       [oven respondsToSelector:@selector(powerUp)] should be_truthy;
    });
});

SPEC_END

Since the house object doesn’t have a -powerUpAppliances method, and the oven object doesn’t have a -powerUp method, the HouseSpec.mm won’t build and we can’t run the oven spec. Again, since this is a compiled language, feel free to comment out the other spec until we get this test failing.

When we focus on the OvenSpec, we have a failing test again. Let’s remedy that:

Oven.h
1
2
3
4
5
@interface Oven : NSObject

- (void)powerUp;

@end
Oven.m
1
2
3
4
5
6
7
8
#import "Oven.h"

@implementation Oven

- (void)powerUp
{ }

@end

Now that we’ve implemented the powerUp method, the OvenSpec tests are green once again. Let’s go back to the HouseSpec. To fix the compile error, we need to add the -powerUpAppliances method:

HouseSpec.h
1
2
3
4
5
6
7
8
9
10
11
#import "Oven.h"

@interface House : NSObject

@property (strong, nonatomic, readonly) Oven *oven;

- (instancetype)initWithOven:(Oven *)oven;

- (void)powerUpAppliances;

@end
House.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#import "House.h"

@interface House ()

@property (strong, nonatomic, readwrite) Oven *oven;

@end

@implementation House

- (instancetype)initWithOven:(Oven *)oven
{
    self = [super init];
    if (self) {
        self.oven = oven;
    }
    return self;
}

- (void)powerUpAppliances
{ }

@end

When we run the tests, now we have a failing test. Let’s fix that test by allowing the oven property to receive the selector:

HouseSpec.h
1
2
3
4
5
6
7
8
9
10
11
#import "Oven.h"

@interface House : NSObject

@property (strong, nonatomic, readonly) Oven *oven;

- (instancetype)initWithOven:(Oven *)oven;

- (void)powerUpAppliances;

@end
House.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#import "House.h"

@interface House ()

@property (strong, nonatomic, readwrite) Oven *oven;

@end

@implementation House

- (instancetype)initWithOven:(Oven *)oven
{
    self = [super init];
    if (self) {
        self.oven = oven;
    }
    return self;
}

- (void)powerUpAppliances
{
    [self.oven powerUp];
}

@end

Bam! Green!

What is the point of creating a fake? Couldn’t you have just spied on the object being tested?

Joe Developer Senior Cowboy Coder

While this could have been done using spies with the Cedar framework, try to use fakes whenever possible. There are three really good reasons why you should use fakes:

  1. Creating a real object can be expensive. While the oven is super simple, what if the object that is being tested has dependencies that have huge memory requirements.
  2. The real object can make network calls, fire notifications, or have complex initializations. When you are testing one object, it’s dependencies don’t need to be used.
  3. Real objects are fragile. What if the oven object became complex? It’s easier to keep things loose if you use a fake object, and can make test refactoring easier in the future.

While I’m going to stop here with the house class, it’s obvious that this could be extended. We could add more appliances, or start to work on some other additions. The house could can be found on Github.

Well there you have it. While the house/oven classes aren’t your average TDD examples, I needed a couple classes that will help make my SuperBurritos crispy on the outside.

Comments