Cuckoo for Cocoa Dev

A blog for all those Cuckoo for Cocoa Development

Behavioral Driven Development (BDD) Using Specta Part 1 - Advanced Techniques

Like most development tasks, eventually you get pulled into different directions while attempting one particular task. This blog post is no different. I’ve taken a brief pause (well it you check the dates it’s not too brief) to cover some additional testing needs, but it’s time to get back on the BDD wagon with some more advanced testing techniques that are necessary when handling app networking.

To make things simple, let’s use AFNetworking to download some JSON from the internet using a sample REST api from JSONPlaceholder. This will also show how to handle completion and failure block testing as well. CuckooForSpecta is already setup on Github and will serve as the example for this post.

Since the JSONPlaceholder api has some JSON that has users, let’s construct a CuckooUser spec to get us started.

CuckooUserSpec.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#import <Specta/Specta.h>
#import <Expecta/Expecta.h>
#import <OCMock/OCMock.h>
#import "CuckooUser.h"

SpecBegin(CuckooUserSpec)

describe(@"CuckooUser", ^{
    __block CuckooUser *cuckooUser;

    beforeEach(^{
        cuckooUser = [[CuckooUser alloc] init];
    });

    it(@"exists", ^{
        expect(cuckooUser).toNot.beNil();
    });
});

SpecEnd

To make these pass, create the class CuckooUser:

CuckoUser.h/.m
1
2
3
4
5
6
7
@interface CuckooUser: NSObject
@end

#import "CuckooUser.h"

@implementation CuckooUser
@end

Now we want the user to have a few properties: name, username and email address, let’s write those tests:

CuckooUserSpec.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
26
describe(@"CuckooUser", ^{
    __block CuckooUser *cuckooUser;

    beforeEach(^{
        cuckooUser = [[CuckooUser alloc] init];
    });

    it(@"exists", ^{
        expect(cuckooUser).toNot.beNil();
    });

    it(@"has a name", ^{
        cuckooUser.name = @"Rocket";
        expect(cuckooUser.name).to.equal(@"Rocket");
    });

    it(@"has a username", ^{
        cuckooUser.username = @"rocketRaccoon";
        expect(cuckooUser.username).to.equal(@"rocketRaccoon");
    });

    it(@"has an email", ^{
        cuckooUser.email = @"rocketRaccoon@guardiansOfTheGalaxy.com";
        expect(cuckooUser.email).to.equal(@"rocketRaccoon@guardiansOfTheGalaxy.com");
    });
});

We can get these tests to pass by adding the following properties:

CuckooUser.h
1
2
3
4
5
6
7
@interface CuckooUser : NSObject

@property (copy, nonatomic) NSString *name;
@property (copy, nonatomic) NSString *username;
@property (copy, nonatomic) NSString *email;

@end

Now that those tests pass, let’s move on to the networking part. In order to do a GET request from the JSON placeholder api, we need an instance of AFHTTPSessionManager. Let’s inject one of those in (a fake for now using OCMock) into a new init method by creating a failing test:

CuckooUserSpec.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
26
27
28
29
30
31
32
describe(@"CuckooUser", ^{
    __block CuckooUser *cuckooUser;
    __block AFHTTPSessionManager *httpSessionManager;

    beforeEach(^{
        httpSessionManager = OCMClassMock([AFHTTPSessionManager class]);
        cuckooUser = [[CuckooUser alloc] initWithHTTPSessionManager:httpSessionManager];
    });

    it(@"exists", ^{
        expect(cuckooUser).toNot.beNil();
    });

    it(@"has a name", ^{
        cuckooUser.name = @"Rocket";
        expect(cuckooUser.name).to.equal(@"Rocket");
    });

    it(@"has a username", ^{
        cuckooUser.username = @"rocketRaccoon";
        expect(cuckooUser.username).to.equal(@"rocketRaccoon");
    });

    it(@"has an email", ^{
        cuckooUser.email = @"rocketRaccoon@guardiansOfTheGalaxy.com";
        expect(cuckooUser.email).to.equal(@"rocketRaccoon@guardiansOfTheGalaxy.com");
    });

    it(@"has an AFHTTPSessionManager", ^{
        expect(cuckooUser.httpSessionManager).toNot.beNil();
    });
});

Since this won’t build at the moment, let’s add the -init method and property for the AFHTTPSessionManager:

CuckooUser.h/.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
@class AFHTTPSessionManager;

@interface CuckooUser : NSObject

@property (copy, nonatomic) NSString *name;
@property (copy, nonatomic) NSString *username;
@property (copy, nonatomic) NSString *email;
@property (strong, nonatomic) AFHTTPSessionManager *httpSessionManager;

- (instancetype)initWithHTTPSessionManager:(AFHTTPSessionManager *)httpSessionManager;

@end

@implementation CuckooUser

- (instancetype)initWithHTTPSessionManager:(AFHTTPSessionManager *)httpSessionManager
{
    self = [super init];
    if (self) {
    }
    return self;
}

@end

When we run the tests now, we will have a failing test, as the httpSessionManager property will return nil. Fix, Fix, Fix:

BAM!!! We are moving along. Since the session manager is going to be injected, we will need to make an assumption about the state of the manager. Since we only care about using it, it will be the responsibly of the class or object to make sure that it has a baseURL that points to the JSONPlaceholder api correctly. At this point with our CuckooUser, we only care that we hit the correct endpoint to get our user. For sheer simplicity we will just have the CuckooUser be the user with an id = 1 from the JSONPlaceholder api. Here’s the test:

CuckooUserSpec.m
1
2
3
4
5
6
7
8
9
10
11
describe(@"CuckooUser", ^{

  // ...other tests...

  it(@"hits the /users/1 endpoint for user JSON", ^{
        OCMVerify([httpSessionManager GET:@"/users/1"
                               parameters:[OCMArg any]
                                  success:[OCMArg any]
                                  failure:[OCMArg any]]);
    });
});

The easiest thing to make this pass is call -GET:parameters:success:failure: using the proper endpoint with nil arguments for everything else:

CuckooUser.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@implementation CuckooUser

- (instancetype)initWithHTTPSessionManager:(AFHTTPSessionManager *)httpSessionManager
{
    self = [super init];
    if (self) {
        self.httpSessionManager = httpSessionManager;

        [self.httpSessionManager GET:@"/users/1"
                          parameters:nil
                             success:nil
                             failure:nil];
    }
    return self;
}

@end

Ok here is where we are at a standstill. We are doing great so far, because if you’ve noticed, we are NOT hitting any real endpoints with AFNetworking. This is good for 2 reasons:

  1. We don’t have to rely on these apis existing. This helps a lot if you have your own backend you are maintaining and might be down for some reason (or any backend for that matter).

  2. Our tests will be super speedy because we won’t have to rely on making an actual request to the api itself.

However, is it possible to test what happens on success an failure without having to wait for the asynchronous call backs? Of course, but it requires some neat little subclassing tricks to do the job!

To handle the callback blocks themselves, we need to create a way to store these and then call them directly at our leisure without having an unpredictable way to wait for them. To do this, we create a simple subclass of our friend, the AFHTTPSessionManager, override the methods/properties we are using and store them off for later usage with additional properties:

RealFakeAFHTTPSessionManager.h/.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
26
27
28
29
#import "AFHTTPSessionManager.h"

@interface RealFakeAFHTTPSessionManager : AFHTTPSessionManager

@property (copy, nonatomic) NSString *getURLString;
@property (strong, nonatomic) NSDictionary *getParameters;
@property (copy, nonatomic) void (^getSuccessBlock)(NSURLSessionDataTask *task, id responseObject);
@property (copy, nonatomic) void (^getFailureBlock)(NSURLSessionDataTask *task, NSError *error);

@end

#import "RealFakeAFHTTPSessionManager.h"

@implementation RealFakeAFHTTPSessionManager

- (NSURLSessionDataTask *)GET:(NSString *)URLString
                   parameters:(id)parameters
                      success:(void (^)(NSURLSessionDataTask *task, id responseObject))success
                      failure:(void (^)(NSURLSessionDataTask *task, NSError *error))failure
{
    self.getURLString = URLString;
    self.getParameters = parameters;
    self.getSuccessBlock = success;
    self.getFailureBlock = failure;

    return nil;
}

@end

WHOA buddy? what the heck is going on here? What are we doing?

Joe Developer Senior Cowboy Coder

It might look like a lot is going on here, but in reality it’s quite simple. We are going to create a customized object that is going to be a fake stand in for our AFHTTPSessionManager (hence it’s name as a ‘Real Fake’). If we do this, then we will hijack calls it makes and hang on to the arguments passed in via public properties. Once we have access to the properties we can use them to validate our code. Let’s see this in action (we will also refactor the previous test to use this new real fake):

CuckooUserSpec.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
26
27
28
29
describe(@"CuckooUser", ^{
  beforeEach(^{
      fakeHttpSessionManager = [[RealFakeAFHTTPSessionManager alloc] init];
      cuckooUser = [[CuckooUser alloc] initWithHTTPSessionManager:fakeHttpSessionManager];
  });

  // ...other tests...

  it(@"hits the /users/1 endpoint for user JSON", ^{
        expect(fakeHttpSessionManager.getURLString).to.equal(@"/users/1");
        expect(fakeHttpSessionManager.getParameters).to.beNil();
    });

  context(@"on success", ^{
        __block NSDictionary *fakeResponse;

        beforeEach(^{
            fakeResponse = @{ @"name": @"Sam Rockwell"};

            if (fakeHttpSessionManager.getSuccessBlock) {
                fakeHttpSessionManager.getSuccessBlock(nil, fakeResponse);
            }
        });

        it(@"sets the name", ^{
            expect(cuckooUser.name).to.equal(@"Sam Rockwell");
        });
    });
});

If you check this last test out you will notice that we are manually calling the success block. Since we are storing off all this information when we “init”, we call call this stuff whenever we want. This makes it very convenient to test network service asynchronous callback blocks without having to wait for them to finish.

In order to trigger the success block, we pass in a response dictionary of our choice. This way we can assert the object under test without having to be dependent on any other objects other than our fakes (or real fakes). Since this fails, lets make it pass:

CuckooUser.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@implementation CuckooUser

- (instancetype)initWithHTTPSessionManager:(AFHTTPSessionManager *)httpSessionManager
{
    self = [super init];
    if (self) {
        self.httpSessionManager = httpSessionManager;

        [self.httpSessionManager GET:@"/users/1"
                          parameters:nil
                             success:^(NSURLSessionDataTask *task, id responseObject) {
                                 self.name = responseObject[@"name"];
                             } failure:nil];
    }
    return self;
}

@end

Let’s now do the username:

CuckooUserSpec.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
26
27
28
29
describe(@"CuckooUser", ^{
  beforeEach(^{
        fakeHttpSessionManager = [[RealFakeAFHTTPSessionManager alloc] init];
        cuckooUser = [[CuckooUser alloc] initWithHTTPSessionManager:fakeHttpSessionManager];
  });

  // ...other tests...

  context(@"on success", ^{
        __block NSDictionary *fakeResponse;

        beforeEach(^{
            fakeResponse = @{ @"name":     @"Sam Rockwell",
                              @"username": @"vMancini"};

            if (fakeHttpSessionManager.getSuccessBlock) {
                fakeHttpSessionManager.getSuccessBlock(nil, fakeResponse);
            }
        });

        it(@"sets the name", ^{
            expect(cuckooUser.name).to.equal(@"Sam Rockwell");
        });

        it(@"sets the username", ^{
            expect(cuckooUser.username).to.equal(@"vMancini");
        });
    });
});

Let’s make it pass:

CuckooUser.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@implementation CuckooUser

- (instancetype)initWithHTTPSessionManager:(AFHTTPSessionManager *)httpSessionManager
{
    self = [super init];
    if (self) {
        self.httpSessionManager = httpSessionManager;

        [self.httpSessionManager GET:@"/users/1"
                          parameters:nil
                             success:^(NSURLSessionDataTask *task, id responseObject) {
                                 self.name = responseObject[@"name"];
                                 self.username = responseObject[@"username"];
                             } failure:nil];
    }
    return self;
}

@end

Finally lets do the email:

CuckooUserSpec.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
26
27
28
29
30
31
32
33
34
describe(@"CuckooUser", ^{
  beforeEach(^{
        fakeHttpSessionManager = [[RealFakeAFHTTPSessionManager alloc] init];
        cuckooUser = [[CuckooUser alloc] initWithHTTPSessionManager:fakeHttpSessionManager];
  });

  // ...other tests...

  context(@"on success", ^{
        __block NSDictionary *fakeResponse;

        beforeEach(^{
            fakeResponse = @{ @"name":     @"Sam Rockwell",
                              @"username": @"vMancini",
                              @"email":    @"vMancini@moon.com" };

            if (fakeHttpSessionManager.getSuccessBlock) {
                fakeHttpSessionManager.getSuccessBlock(nil, fakeResponse);
            }
        });

        it(@"sets the name", ^{
            expect(cuckooUser.name).to.equal(@"Sam Rockwell");
        });

        it(@"sets the username", ^{
            expect(cuckooUser.username).to.equal(@"vMancini");
        });

        it(@"sets the email", ^{
            expect(cuckooUser.email).to.equal(@"vMancini@moon.com");
        });
    });
});

Similarly, we can make it pass by using the response object:

CuckooUser.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@implementation CuckooUser

- (instancetype)initWithHTTPSessionManager:(AFHTTPSessionManager *)httpSessionManager
{
    self = [super init];
    if (self) {
        self.httpSessionManager = httpSessionManager;

        [self.httpSessionManager GET:@"/users/1"
                          parameters:nil
                             success:^(NSURLSessionDataTask *task, id responseObject) {
                                 self.name = responseObject[@"name"];
                                 self.username = responseObject[@"username"];
                                 self.email = responseObject[@"email"];
                             } failure:nil];
    }
    return self;
}

@end

Pretty cool stuff… Let’s create default values for the cuckoo user if there is a failure with the network call:

CuckooUserSpec.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
describe(@"CuckooUser", ^{
  beforeEach(^{
        fakeHttpSessionManager = [[RealFakeAFHTTPSessionManager alloc] init];
        cuckooUser = [[CuckooUser alloc] initWithHTTPSessionManager:fakeHttpSessionManager];
  });

  // ...other tests...

  context(@"on failure", ^{
        beforeEach(^{
            if (fakeHttpSessionManager.getFailureBlock) {
                fakeHttpSessionManager.getFailureBlock(nil,nil);
            }
        });

        it(@"sets default user values", ^{
            expect(cuckooUser.name).to.equal(@"Peter Parker");
            expect(cuckooUser.username).to.equal(@"p_parker");
            expect(cuckooUser.email).to.equal(@"p_parker@amazing.com");
        });
    });
});

Easy:

CuckooUser.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
@implementation CuckooUser

- (instancetype)initWithHTTPSessionManager:(AFHTTPSessionManager *)httpSessionManager
{
    self = [super init];
    if (self) {
        self.httpSessionManager = httpSessionManager;

        [self.httpSessionManager GET:@"/users/1"
                          parameters:nil
                             success:^(NSURLSessionDataTask *task, id responseObject) {
                                 self.name = responseObject[@"name"];
                                 self.username = responseObject[@"username"];
                                 self.email = responseObject[@"email"];
                             } failure:^(NSURLSessionDataTask *task, NSError *error) {
                                 self.name = @"Peter Parker";
                                 self.username = @"p_parker";
                                 self.email = @"p_parker@amazing.com";
                             }];
    }
    return self;
}

@end

To finish up, let’s post a notification when we are done updating the CuckooUser. This makes it simple to use in a view controller. Since the NSNotificationCenter is accessed using a class method, let’s stub it and return a fake. Once we have a fake in place, we can verify that fake has received the -postNotificationName: method with the correct name and object:

CuckooUserSpec.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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
describe(@"CuckooUser", ^{
  beforeEach(^{
      fakeHttpSessionManager = [[RealFakeAFHTTPSessionManager alloc] init];
      cuckooUser = [[CuckooUser alloc] initWithHTTPSessionManager:fakeHttpSessionManager];
  });

  // ...other tests...

  context(@"on success", ^{
        __block NSDictionary *fakeResponse;
        __block id fakeNSNotificationCenter;

        beforeEach(^{
            fakeNSNotificationCenter = OCMClassMock([NSNotificationCenter class]);
            OCMStub([fakeNSNotificationCenter defaultCenter]).andReturn(fakeNSNotificationCenter);

            fakeResponse = @{ @"name":     @"Sam Rockwell",
                              @"username": @"vMancini",
                              @"email":    @"vMancini@moon.com" };

            if (fakeHttpSessionManager.getSuccessBlock) {
                fakeHttpSessionManager.getSuccessBlock(nil, fakeResponse);
            }
        });

        it(@"sets the name", ^{
            expect(cuckooUser.name).to.equal(@"Sam Rockwell");
        });

        it(@"sets the username", ^{
            expect(cuckooUser.username).to.equal(@"vMancini");
        });

        it(@"sets the email", ^{
            expect(cuckooUser.email).to.equal(@"vMancini@moon.com");
        });

        it(@"it posts a CuckooUserUpdatedNotification", ^{
            OCMVerify([fakeNSNotificationCenter postNotificationName:@"CuckooUserUpdatedNotification"
                                                              object:cuckooUser]);
        });
    });

    // ...other tests...

});

Post it!!!

CuckooUserSpec.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
26
27
@implementation CuckooUser

- (instancetype)initWithHTTPSessionManager:(AFHTTPSessionManager *)httpSessionManager
{
    self = [super init];
    if (self) {
        self.httpSessionManager = httpSessionManager;

        [self.httpSessionManager GET:@"/users/1"
                          parameters:nil
                             success:^(NSURLSessionDataTask *task, id responseObject) {
                                 self.name = responseObject[@"name"];
                                 self.username = responseObject[@"username"];
                                 self.email = responseObject[@"email"];

                                 [[NSNotificationCenter defaultCenter] postNotificationName:@"CuckooUserUpdatedNotification"
                                                                                     object:self];
                             } failure:^(NSURLSessionDataTask *task, NSError *error) {
                                 self.name = @"Peter Parker";
                                 self.username = @"p_parker";
                                 self.email = @"p_parker@amazing.com";
                             }];
    }
    return self;
}

@end

This technique for testing can be very powerful. It doesn’t just stop at faking networking calls, but can be used for any objects that require expensive setup, or dependencies that require slow operations such as saving to disk or long asynchronous tasks. This proves to be additionally useful if you don’t have a networking connection on BART but still want to TDD some code. Well, that is if you aren’t making better usage of the time by napping or reading an excellent book =)

Comments