iOS Behavior-Driven
Development
Testing RESTful Applications with Kiwi and Nocilla
March 9th, 2014
Brian Gesiak
Research Student, The University of Tokyo
@modocache #startup_ios
Today
• Behavior-driven development (BDD)
• iOS behavior-driven development
• Kiwi
• Testing asynchronous networking
• Nocilla
Test-Driving Network Code
Motivation
• Let’s say we want to display a user’s

repositories on GitHub
• We can GET JSON from the GitHub
API
• https://api.github.com/users/
{{ username }}/repos.json
Test-Driving Network Code
Motivation

/// GET /users/:username/repos
!

[
{
"id": 1296269,
"name": "Hello-World",
"description": "My first repo!",
/* ... */
}
]
Test-Driving Network Code
Demonstration
Building the App
Behavior-Driven Development Using Kiwi
• Behavior-driven development (BDD) is an extension of

test-driven development
Test-Driven Development
Test-Driven Development
• Red: Write a test and watch it fail
Test-Driven Development
• Red: Write a test and watch it fail
• Green: Pass the test (by writing as little code as possible)
Test-Driven Development
• Red: Write a test and watch it fail
• Green: Pass the test (by writing as little code as possible)
• Refactor: Remove duplication
Test-Driven Development
• Red: Write a test and watch it fail
• Green: Pass the test (by writing as little code as possible)
• Refactor: Remove duplication
• Repeat
Example of iOS TDD Using XCTest
Example of iOS TDD Using XCTest
// Create an XCTestCase subclass
@interface GHVRepoCollectionTests : XCTestCase
// The subject of our tests
@property (nonatomic, strong) GHVCollection *collection;
@end
!
@implementation GHVRepoCollectionTests
// Set up the `repos` collection for each test
- (void)setUp {
[super setUp];
self.collection = [GHVCollection new];
}
// Add a test
- (void)testAddingARepo {
// Add a repo
[self.collection addRepo:[GHVRepo new]];
!
// Assert the number of repos is now one
XCTAssertEqual([self.collection.repos count], 1,
@"Expected one repository");
}
@end
Example of iOS TDD Using XCTest
// Create an XCTestCase subclass
@interface GHVRepoCollectionTests : XCTestCase
// The subject of our tests
@property (nonatomic, strong) GHVCollection *collection;
@end
!
@implementation GHVRepoCollectionTests
// Set up the `repos` collection for each test
- (void)setUp {
[super setUp];
self.collection = [GHVCollection new];
}
// Add a test
- (void)testAddingARepo {
// Add a repo
[self.collection addRepo:[GHVRepo new]];
!
// Assert the number of repos is now one
XCTAssertEqual([self.collection.repos count], 1,
@"Expected one repository");
}
@end
Example of iOS TDD Using XCTest
// Create an XCTestCase subclass
@interface GHVRepoCollectionTests : XCTestCase
// The subject of our tests
@property (nonatomic, strong) GHVCollection *collection;
@end
!
@implementation GHVRepoCollectionTests
// Set up the `repos` collection for each test
- (void)setUp {
[super setUp];
self.collection = [GHVCollection new];
}
// Add a test
- (void)testAddingARepo {
// Add a repo
[self.collection addRepo:[GHVRepo new]];
!
// Assert the number of repos is now one
XCTAssertEqual([self.collection.repos count], 1,
@"Expected one repository");
}
@end
Example of iOS TDD Using XCTest
// Create an XCTestCase subclass
@interface GHVRepoCollectionTests : XCTestCase
// The subject of our tests
@property (nonatomic, strong) GHVCollection *collection;
@end
!
@implementation GHVRepoCollectionTests
// Set up the `repos` collection for each test
- (void)setUp {
[super setUp];
self.collection = [GHVCollection new];
}
// Add a test
- (void)testAddingARepo {
// Add a repo
[self.collection addRepo:[GHVRepo new]];
!
// Assert the number of repos is now one
XCTAssertEqual([self.collection.repos count], 1,
@"Expected one repository");
}
@end
Example of iOS TDD Using XCTest
// Create an XCTestCase subclass
@interface GHVRepoCollectionTests : XCTestCase
// The subject of our tests
@property (nonatomic, strong) GHVCollection *collection;
@end
!
@implementation GHVRepoCollectionTests
// Set up the `repos` collection for each test
- (void)setUp {
[super setUp];
self.collection = [GHVCollection new];
}
// Add a test
- (void)testAddingARepo {
// Add a repo
[self.collection addRepo:[GHVRepo new]];
!
// Assert the number of repos is now one
XCTAssertEqual([self.collection.repos count], 1,
@"Expected one repository");
}
@end
Example of iOS TDD Using XCTest
// Create an XCTestCase subclass
@interface GHVRepoCollectionTests : XCTestCase
// The subject of our tests
@property (nonatomic, strong) GHVCollection *collection;
@end
!
@implementation GHVRepoCollectionTests
// Set up the `repos` collection for each test
- (void)setUp {
[super setUp];
self.collection = [GHVCollection new];
}
// Add a test
- (void)testAddingARepo {
// Add a repo
[self.collection addRepo:[GHVRepo new]];
!
// Assert the number of repos is now one
XCTAssertEqual([self.collection.repos count], 1,
@"Expected one repository");
}
@end
Behavior-Driven Development
• Answers the question: “What do I test?”
• Behavioral tests don’t test the implementation, they

specify the behavior
iOS BDD Using Kiwi
iOS BDD Using Kiwi
// Describe how the `GHVCollection` class behaves
describe(@"GHVCollection", ^{
// Setup up the collection for each test
__block GHVCollection *collection = nil;
beforeEach(^{
collection = [GHVCollection new];
});
!
// Describe how the `-addRepo:` method behaves
describe(@"-addRepo:", ^{
context(@"after adding a repo", ^{
// Add a repo before each test
beforeEach(^{
[collection addRepo:[GHVRepo new]];
});
// Test the method behaves correctly
it(@"has a repo count of one", ^{
[[collection.repos should] haveCountOf:1];
});
});
});
});
iOS BDD Using Kiwi
// Describe how the `GHVCollection` class behaves
describe(@"GHVCollection", ^{
// Setup up the collection for each test
__block GHVCollection *collection = nil;
beforeEach(^{
collection = [GHVCollection new];
});
!
// Describe how the `-addRepo:` method behaves
describe(@"-addRepo:", ^{
context(@"after adding a repo", ^{
// Add a repo before each test
beforeEach(^{
[collection addRepo:[GHVRepo new]];
});
// Test the method behaves correctly
it(@"has a repo count of one", ^{
[[collection.repos should] haveCountOf:1];
});
});
});
});
iOS BDD Using Kiwi
// Describe how the `GHVCollection` class behaves
describe(@"GHVCollection", ^{
// Setup up the collection for each test
__block GHVCollection *collection = nil;
beforeEach(^{
collection = [GHVCollection new];
});
!
// Describe how the `-addRepo:` method behaves
describe(@"-addRepo:", ^{
context(@"after adding a repo", ^{
// Add a repo before each test
beforeEach(^{
[collection addRepo:[GHVRepo new]];
});
// Test the method behaves correctly
it(@"has a repo count of one", ^{
[[collection.repos should] haveCountOf:1];
});
});
});
});
iOS BDD Using Kiwi
// Describe how the `GHVCollection` class behaves
describe(@"GHVCollection", ^{
// Setup up the collection for each test
__block GHVCollection *collection = nil;
beforeEach(^{
collection = [GHVCollection new];
});
!
// Describe how the `-addRepo:` method behaves
describe(@"-addRepo:", ^{
context(@"after adding a repo", ^{
// Add a repo before each test
beforeEach(^{
[collection addRepo:[GHVRepo new]];
});
// Test the method behaves correctly
it(@"has a repo count of one", ^{
[[collection.repos should] haveCountOf:1];
});
});
});
});
iOS BDD Using Kiwi
// Describe how the `GHVCollection` class behaves
describe(@"GHVCollection", ^{
// Setup up the collection for each test
__block GHVCollection *collection = nil;
beforeEach(^{
collection = [GHVCollection new];
});
!
// Describe how the `-addRepo:` method behaves
describe(@"-addRepo:", ^{
context(@"after adding a repo", ^{
// Add a repo before each test
beforeEach(^{
[collection addRepo:[GHVRepo new]];
});
// Test the method behaves correctly
it(@"has a repo count of one", ^{
[[collection.repos should] haveCountOf:1];
});
});
});
});
iOS BDD Using Kiwi
// Describe how the `GHVCollection` class behaves
describe(@"GHVCollection", ^{
// Setup up the collection for each test
__block GHVCollection *collection = nil;
beforeEach(^{
collection = [GHVCollection new];
});
!
// Describe how the `-addRepo:` method behaves
describe(@"-addRepo:", ^{
context(@"after adding a repo", ^{
// Add a repo before each test
beforeEach(^{
[collection addRepo:[GHVRepo new]];
});
// Test the method behaves correctly
it(@"has a repo count of one", ^{
[[collection.repos should] haveCountOf:1];
});
});
});
});
Kiwi Benefits
Kiwi Benefits
• An unlimited amount of setup and teardown
Kiwi Benefits
• An unlimited amount of setup and teardown

beforeEach(^{
beforeAll(^{
afterEach(^{
afterAll(^{

/*
/*
/*
/*

...
...
...
...

*/
*/
*/
*/

});
});
});
});
Kiwi Benefits
• An unlimited amount of setup and teardown

beforeEach(^{
beforeAll(^{
afterEach(^{
afterAll(^{
• Mocks and stubs included

/*
/*
/*
/*

...
...
...
...

*/
*/
*/
*/

});
});
});
});
Kiwi Benefits
• An unlimited amount of setup and teardown

beforeEach(^{
beforeAll(^{
afterEach(^{
afterAll(^{

/*
/*
/*
/*

...
...
...
...

*/
*/
*/
*/

});
});
});
});

• Mocks and stubs included

[collection stub:@selector(addRepo:)];
Kiwi Benefits
• An unlimited amount of setup and teardown

beforeEach(^{
beforeAll(^{
afterEach(^{
afterAll(^{

/*
/*
/*
/*

...
...
...
...

*/
*/
*/
*/

});
});
});
});

• Mocks and stubs included

[collection stub:@selector(addRepo:)];
• Asynchronous testing support
Kiwi Benefits
• An unlimited amount of setup and teardown

beforeEach(^{
beforeAll(^{
afterEach(^{
afterAll(^{

/*
/*
/*
/*

...
...
...
...

*/
*/
*/
*/

});
});
});
});

• Mocks and stubs included

[collection stub:@selector(addRepo:)];
• Asynchronous testing support

[[collection.repos shouldEventually] haveCountOf:2];
Kiwi Benefits
• An unlimited amount of setup and teardown

beforeEach(^{
beforeAll(^{
afterEach(^{
afterAll(^{

/*
/*
/*
/*

...
...
...
...

*/
*/
*/
*/

});
});
});
});

• Mocks and stubs included

[collection stub:@selector(addRepo:)];
• Asynchronous testing support

[[collection.repos shouldEventually] haveCountOf:2];
• More readable than XCTest
Our First Failing Test
Our First Failing Test
/// GHVAPIClientSpec.m
!

it(@"gets repositories", ^{
// The repos returned by the API
__block NSArray *allRepos = nil;
!

// Fetch the repos from the API
[client allRepositoriesForUsername:@"modocache"
success:^(NSArray *repos) {
// Set the repos
allRepos = repos;
} failure:nil];
!

// Assert that the repos have been set
[[expectFutureValue(allRepos) shouldEventually]
haveCountOf:10];
});
Our First Failing Test
/// GHVAPIClientSpec.m
!

it(@"gets repositories", ^{
// The repos returned by the API
__block NSArray *allRepos = nil;
!

// Fetch the repos from the API
[client allRepositoriesForUsername:@"modocache"
success:^(NSArray *repos) {
// Set the repos
allRepos = repos;
} failure:nil];
!

// Assert that the repos have been set
[[expectFutureValue(allRepos) shouldEventually]
haveCountOf:10];
});
Our First Failing Test
/// GHVAPIClientSpec.m
!

it(@"gets repositories", ^{
// The repos returned by the API
__block NSArray *allRepos = nil;
!

// Fetch the repos from the API
[client allRepositoriesForUsername:@"modocache"
success:^(NSArray *repos) {
// Set the repos
allRepos = repos;
} failure:nil];
!

// Assert that the repos have been set
[[expectFutureValue(allRepos) shouldEventually]
haveCountOf:10];
});
Our First Failing Test
/// GHVAPIClientSpec.m
!

it(@"gets repositories", ^{
// The repos returned by the API
__block NSArray *allRepos = nil;
!

// Fetch the repos from the API
[client allRepositoriesForUsername:@"modocache"
success:^(NSArray *repos) {
// Set the repos
allRepos = repos;
} failure:nil];
!

// Assert that the repos have been set
[[expectFutureValue(allRepos) shouldEventually]
haveCountOf:10];
});
Our First Failing Test
/// GHVAPIClientSpec.m
!

it(@"gets repositories", ^{
// The repos returned by the API
__block NSArray *allRepos = nil;
!

// Fetch the repos from the API
[client allRepositoriesForUsername:@"modocache"
success:^(NSArray *repos) {
// Set the repos
allRepos = repos;
} failure:nil];
!

// Assert that the repos have been set
[[expectFutureValue(allRepos) shouldEventually]
haveCountOf:10];
});
Our First Failing Test
/// GHVAPIClientSpec.m
!

it(@"gets repositories", ^{
// The repos returned by the API
__block NSArray *allRepos = nil;
!

// Fetch the repos from the API
[client allRepositoriesForUsername:@"modocache"
success:^(NSArray *repos) {
// Set the repos
allRepos = repos;
} failure:nil];
!

// Assert that the repos have been set
[[expectFutureValue(allRepos) shouldEventually]
haveCountOf:10];
});
Going Green
Going Green
/// GHVAPIClient.m

!

// Create a request operation manager pointing at the GitHub API
NSString *urlString = @"https://api.github.com/";
NSURL *baseURL = [NSURL URLWithString:urlString];
AFHTTPRequestOperationManager *manager =
[[AFHTTPRequestOperationManager alloc] initWithBaseURL:baseURL];

!

// The manager should serialize the response as JSON
manager.requestSerializer = [AFJSONRequestSerializer serializer];

!

// Send a request to GET /users/:username/repos
[manager GET:[NSString stringWithFormat:@"users/%@/repos",
username]
parameters:nil
success:^(AFHTTPRequestOperation *operation,
id responseObject) {
// Send response object to success block
success(responseObject);
}
failure:nil];
}
Going Green
/// GHVAPIClient.m

!

// Create a request operation manager pointing at the GitHub API
NSString *urlString = @"https://api.github.com/";
NSURL *baseURL = [NSURL URLWithString:urlString];
AFHTTPRequestOperationManager *manager =
[[AFHTTPRequestOperationManager alloc] initWithBaseURL:baseURL];

!

// The manager should serialize the response as JSON
manager.requestSerializer = [AFJSONRequestSerializer serializer];

!

// Send a request to GET /users/:username/repos
[manager GET:[NSString stringWithFormat:@"users/%@/repos",
username]
parameters:nil
success:^(AFHTTPRequestOperation *operation,
id responseObject) {
// Send response object to success block
success(responseObject);
}
failure:nil];
}
Going Green
/// GHVAPIClient.m

!

// Create a request operation manager pointing at the GitHub API
NSString *urlString = @"https://api.github.com/";
NSURL *baseURL = [NSURL URLWithString:urlString];
AFHTTPRequestOperationManager *manager =
[[AFHTTPRequestOperationManager alloc] initWithBaseURL:baseURL];

!

// The manager should serialize the response as JSON
manager.requestSerializer = [AFJSONRequestSerializer serializer];

!

// Send a request to GET /users/:username/repos
[manager GET:[NSString stringWithFormat:@"users/%@/repos",
username]
parameters:nil
success:^(AFHTTPRequestOperation *operation,
id responseObject) {
// Send response object to success block
success(responseObject);
}
failure:nil];
}
Going Green
/// GHVAPIClient.m

!

// Create a request operation manager pointing at the GitHub API
NSString *urlString = @"https://api.github.com/";
NSURL *baseURL = [NSURL URLWithString:urlString];
AFHTTPRequestOperationManager *manager =
[[AFHTTPRequestOperationManager alloc] initWithBaseURL:baseURL];

!

// The manager should serialize the response as JSON
manager.requestSerializer = [AFJSONRequestSerializer serializer];

!

// Send a request to GET /users/:username/repos
[manager GET:[NSString stringWithFormat:@"users/%@/repos",
username]
parameters:nil
success:^(AFHTTPRequestOperation *operation,
id responseObject) {
// Send response object to success block
success(responseObject);
}
failure:nil];
}
Going Green
/// GHVAPIClient.m

!

// Create a request operation manager pointing at the GitHub API
NSString *urlString = @"https://api.github.com/";
NSURL *baseURL = [NSURL URLWithString:urlString];
AFHTTPRequestOperationManager *manager =
[[AFHTTPRequestOperationManager alloc] initWithBaseURL:baseURL];

!

// The manager should serialize the response as JSON
manager.requestSerializer = [AFJSONRequestSerializer serializer];

!

// Send a request to GET /users/:username/repos
[manager GET:[NSString stringWithFormat:@"users/%@/repos",
username]
parameters:nil
success:^(AFHTTPRequestOperation *operation,
id responseObject) {
// Send response object to success block
success(responseObject);
}
failure:nil];
}
Problems with our Test
• The test has external dependencies
• It’ll fail if the GitHub API is down
• It’ll fail if run without an internet connection
• It’ll fail if the response is too slow
• The test is slow
• It sends a request every time it’s run
HTTP Stubbing
Eliminating external dependencies
stubRequest(@"GET",
@“https://api.github.com/"
@“users/modocache/repos")
.andReturn(200)
.withHeaders(@{@"Content-Type": @"application/json"})
.withBody(@"["repo-1"]");
!

GHVAPIClient *client = [GHVAPIClient new];
!

// ...
!

[[expectFutureValue(allRepos) shouldEventually]
haveCountOf:1];
HTTP Stubbing
Eliminating external dependencies
stubRequest(@"GET",
@“https://api.github.com/"
@“users/modocache/repos")
.andReturn(200)
.withHeaders(@{@"Content-Type": @"application/json"})
.withBody(@"["repo-1"]");
!

GHVAPIClient *client = [GHVAPIClient new];
!

// ...
!

[[expectFutureValue(allRepos) shouldEventually]
haveCountOf:1];
HTTP Stubbing
Eliminating external dependencies
stubRequest(@"GET",
@“https://api.github.com/"
@“users/modocache/repos")
.andReturn(200)
.withHeaders(@{@"Content-Type": @"application/json"})
.withBody(@"["repo-1"]");
!

GHVAPIClient *client = [GHVAPIClient new];
!

// ...
!

[[expectFutureValue(allRepos) shouldEventually]
haveCountOf:1];
HTTP Stubbing
Eliminating external dependencies
stubRequest(@"GET",
@“https://api.github.com/"
@“users/modocache/repos")
.andReturn(200)
.withHeaders(@{@"Content-Type": @"application/json"})
.withBody(@"["repo-1"]");
!

GHVAPIClient *client = [GHVAPIClient new];
!

// ...
!

[[expectFutureValue(allRepos) shouldEventually]
haveCountOf:1];
HTTP Stubbing
Eliminating external dependencies
stubRequest(@"GET",
@“https://api.github.com/"
@“users/modocache/repos")
.andReturn(200)
.withHeaders(@{@"Content-Type": @"application/json"})
.withBody(@"["repo-1"]");
!

GHVAPIClient *client = [GHVAPIClient new];
!

// ...
!

[[expectFutureValue(allRepos) shouldEventually]
haveCountOf:1];
HTTP Stubbing
Eliminating external dependencies
stubRequest(@"GET",
@“https://api.github.com/"
@“users/modocache/repos")
.andReturn(200)
.withHeaders(@{@"Content-Type": @"application/json"})
.withBody(@"["repo-1"]");
!

GHVAPIClient *client = [GHVAPIClient new];
!

// ...
!

[[expectFutureValue(allRepos) shouldEventually]
haveCountOf:1];
Problems Nocilla Fixes
• The test no longer has external dependencies
• It’ll pass whether the GitHub API is online or not
• It’ll pass even when run offline
• The test is fast
• It still sends a request, but that request is immediately

intercepted and a response is returned
Other Nocilla Features
Other Nocilla Features
• Stub HTTP requests using regular expressions
Other Nocilla Features
• Stub HTTP requests using regular expressions
stubRequest(@"GET",
@"https://api.github.com/"
@"users/(.*?)/repos".regex)
Other Nocilla Features
• Stub HTTP requests using regular expressions
stubRequest(@"GET",
@"https://api.github.com/"
@"users/(.*?)/repos".regex)

• Return errors, such as for poor internet connection
Other Nocilla Features
• Stub HTTP requests using regular expressions
stubRequest(@"GET",
@"https://api.github.com/"
@"users/(.*?)/repos".regex)

• Return errors, such as for poor internet connection
NSError *error =
[NSError errorWithDomain:NSURLErrorDomain
code:29
userInfo:@{NSLocalizedDescriptionKey: @"Uh-oh!"}];
stubRequest(@"GET", @"...")
.andFailWithError(error);
Takeaways
Takeaways
• Readable, behavior-driven, asynchronous tests with Kiwi
• https://github.com/allending/Kiwi
Takeaways
• Readable, behavior-driven, asynchronous tests with Kiwi
• https://github.com/allending/Kiwi

pod "Kiwi/XCTest"
Takeaways
• Readable, behavior-driven, asynchronous tests with Kiwi
• https://github.com/allending/Kiwi

pod "Kiwi/XCTest"
• Eliminate network dependencies with Nocilla
• https://github.com/luisobo/Nocilla
Takeaways
• Readable, behavior-driven, asynchronous tests with Kiwi
• https://github.com/allending/Kiwi

pod "Kiwi/XCTest"
• Eliminate network dependencies with Nocilla
• https://github.com/luisobo/Nocilla

pod "Nocilla"
Questions?
@modocache #startup_ios
Questions?
@modocache #startup_ios

describe(@"this talk", ^{
context(@"after presenting the slides", ^{
it(@"moves to Q&A", ^{
[[you should] askQuestions];
[[you shouldEventually]
receive:@selector(stop)];
});
});
});
Questions?
@modocache #startup_ios

describe(@"this talk", ^{
context(@"after presenting the slides", ^{
it(@"moves to Q&A", ^{
[[you should] askQuestions];
[[you shouldEventually]
receive:@selector(stop)];
});
});
});
Questions?
@modocache #startup_ios

describe(@"this talk", ^{
context(@"after presenting the slides", ^{
it(@"moves to Q&A", ^{
[[you should] askQuestions];
[[you shouldEventually]
receive:@selector(stop)];
});
});
});
Questions?
@modocache #startup_ios

describe(@"this talk", ^{
context(@"after presenting the slides", ^{
it(@"moves to Q&A", ^{
[[you should] askQuestions];
[[you shouldEventually]
receive:@selector(stop)];
});
});
});
Questions?
@modocache #startup_ios

describe(@"this talk", ^{
context(@"after presenting the slides", ^{
it(@"moves to Q&A", ^{
[[you should] askQuestions];
[[you shouldEventually]
receive:@selector(stop)];
});
});
});

iOS Behavior-Driven Development

  • 1.
    iOS Behavior-Driven Development Testing RESTfulApplications with Kiwi and Nocilla March 9th, 2014 Brian Gesiak Research Student, The University of Tokyo @modocache #startup_ios
  • 2.
    Today • Behavior-driven development(BDD) • iOS behavior-driven development • Kiwi • Testing asynchronous networking • Nocilla
  • 3.
    Test-Driving Network Code Motivation •Let’s say we want to display a user’s repositories on GitHub • We can GET JSON from the GitHub API • https://api.github.com/users/ {{ username }}/repos.json
  • 4.
    Test-Driving Network Code Motivation ///GET /users/:username/repos ! [ { "id": 1296269, "name": "Hello-World", "description": "My first repo!", /* ... */ } ]
  • 5.
  • 6.
    Building the App Behavior-DrivenDevelopment Using Kiwi • Behavior-driven development (BDD) is an extension of test-driven development
  • 7.
  • 8.
    Test-Driven Development • Red:Write a test and watch it fail
  • 9.
    Test-Driven Development • Red:Write a test and watch it fail • Green: Pass the test (by writing as little code as possible)
  • 10.
    Test-Driven Development • Red:Write a test and watch it fail • Green: Pass the test (by writing as little code as possible) • Refactor: Remove duplication
  • 11.
    Test-Driven Development • Red:Write a test and watch it fail • Green: Pass the test (by writing as little code as possible) • Refactor: Remove duplication • Repeat
  • 12.
    Example of iOSTDD Using XCTest
  • 13.
    Example of iOSTDD Using XCTest // Create an XCTestCase subclass @interface GHVRepoCollectionTests : XCTestCase // The subject of our tests @property (nonatomic, strong) GHVCollection *collection; @end ! @implementation GHVRepoCollectionTests // Set up the `repos` collection for each test - (void)setUp { [super setUp]; self.collection = [GHVCollection new]; } // Add a test - (void)testAddingARepo { // Add a repo [self.collection addRepo:[GHVRepo new]]; ! // Assert the number of repos is now one XCTAssertEqual([self.collection.repos count], 1, @"Expected one repository"); } @end
  • 14.
    Example of iOSTDD Using XCTest // Create an XCTestCase subclass @interface GHVRepoCollectionTests : XCTestCase // The subject of our tests @property (nonatomic, strong) GHVCollection *collection; @end ! @implementation GHVRepoCollectionTests // Set up the `repos` collection for each test - (void)setUp { [super setUp]; self.collection = [GHVCollection new]; } // Add a test - (void)testAddingARepo { // Add a repo [self.collection addRepo:[GHVRepo new]]; ! // Assert the number of repos is now one XCTAssertEqual([self.collection.repos count], 1, @"Expected one repository"); } @end
  • 15.
    Example of iOSTDD Using XCTest // Create an XCTestCase subclass @interface GHVRepoCollectionTests : XCTestCase // The subject of our tests @property (nonatomic, strong) GHVCollection *collection; @end ! @implementation GHVRepoCollectionTests // Set up the `repos` collection for each test - (void)setUp { [super setUp]; self.collection = [GHVCollection new]; } // Add a test - (void)testAddingARepo { // Add a repo [self.collection addRepo:[GHVRepo new]]; ! // Assert the number of repos is now one XCTAssertEqual([self.collection.repos count], 1, @"Expected one repository"); } @end
  • 16.
    Example of iOSTDD Using XCTest // Create an XCTestCase subclass @interface GHVRepoCollectionTests : XCTestCase // The subject of our tests @property (nonatomic, strong) GHVCollection *collection; @end ! @implementation GHVRepoCollectionTests // Set up the `repos` collection for each test - (void)setUp { [super setUp]; self.collection = [GHVCollection new]; } // Add a test - (void)testAddingARepo { // Add a repo [self.collection addRepo:[GHVRepo new]]; ! // Assert the number of repos is now one XCTAssertEqual([self.collection.repos count], 1, @"Expected one repository"); } @end
  • 17.
    Example of iOSTDD Using XCTest // Create an XCTestCase subclass @interface GHVRepoCollectionTests : XCTestCase // The subject of our tests @property (nonatomic, strong) GHVCollection *collection; @end ! @implementation GHVRepoCollectionTests // Set up the `repos` collection for each test - (void)setUp { [super setUp]; self.collection = [GHVCollection new]; } // Add a test - (void)testAddingARepo { // Add a repo [self.collection addRepo:[GHVRepo new]]; ! // Assert the number of repos is now one XCTAssertEqual([self.collection.repos count], 1, @"Expected one repository"); } @end
  • 18.
    Example of iOSTDD Using XCTest // Create an XCTestCase subclass @interface GHVRepoCollectionTests : XCTestCase // The subject of our tests @property (nonatomic, strong) GHVCollection *collection; @end ! @implementation GHVRepoCollectionTests // Set up the `repos` collection for each test - (void)setUp { [super setUp]; self.collection = [GHVCollection new]; } // Add a test - (void)testAddingARepo { // Add a repo [self.collection addRepo:[GHVRepo new]]; ! // Assert the number of repos is now one XCTAssertEqual([self.collection.repos count], 1, @"Expected one repository"); } @end
  • 19.
    Behavior-Driven Development • Answersthe question: “What do I test?” • Behavioral tests don’t test the implementation, they specify the behavior
  • 20.
  • 21.
    iOS BDD UsingKiwi // Describe how the `GHVCollection` class behaves describe(@"GHVCollection", ^{ // Setup up the collection for each test __block GHVCollection *collection = nil; beforeEach(^{ collection = [GHVCollection new]; }); ! // Describe how the `-addRepo:` method behaves describe(@"-addRepo:", ^{ context(@"after adding a repo", ^{ // Add a repo before each test beforeEach(^{ [collection addRepo:[GHVRepo new]]; }); // Test the method behaves correctly it(@"has a repo count of one", ^{ [[collection.repos should] haveCountOf:1]; }); }); }); });
  • 22.
    iOS BDD UsingKiwi // Describe how the `GHVCollection` class behaves describe(@"GHVCollection", ^{ // Setup up the collection for each test __block GHVCollection *collection = nil; beforeEach(^{ collection = [GHVCollection new]; }); ! // Describe how the `-addRepo:` method behaves describe(@"-addRepo:", ^{ context(@"after adding a repo", ^{ // Add a repo before each test beforeEach(^{ [collection addRepo:[GHVRepo new]]; }); // Test the method behaves correctly it(@"has a repo count of one", ^{ [[collection.repos should] haveCountOf:1]; }); }); }); });
  • 23.
    iOS BDD UsingKiwi // Describe how the `GHVCollection` class behaves describe(@"GHVCollection", ^{ // Setup up the collection for each test __block GHVCollection *collection = nil; beforeEach(^{ collection = [GHVCollection new]; }); ! // Describe how the `-addRepo:` method behaves describe(@"-addRepo:", ^{ context(@"after adding a repo", ^{ // Add a repo before each test beforeEach(^{ [collection addRepo:[GHVRepo new]]; }); // Test the method behaves correctly it(@"has a repo count of one", ^{ [[collection.repos should] haveCountOf:1]; }); }); }); });
  • 24.
    iOS BDD UsingKiwi // Describe how the `GHVCollection` class behaves describe(@"GHVCollection", ^{ // Setup up the collection for each test __block GHVCollection *collection = nil; beforeEach(^{ collection = [GHVCollection new]; }); ! // Describe how the `-addRepo:` method behaves describe(@"-addRepo:", ^{ context(@"after adding a repo", ^{ // Add a repo before each test beforeEach(^{ [collection addRepo:[GHVRepo new]]; }); // Test the method behaves correctly it(@"has a repo count of one", ^{ [[collection.repos should] haveCountOf:1]; }); }); }); });
  • 25.
    iOS BDD UsingKiwi // Describe how the `GHVCollection` class behaves describe(@"GHVCollection", ^{ // Setup up the collection for each test __block GHVCollection *collection = nil; beforeEach(^{ collection = [GHVCollection new]; }); ! // Describe how the `-addRepo:` method behaves describe(@"-addRepo:", ^{ context(@"after adding a repo", ^{ // Add a repo before each test beforeEach(^{ [collection addRepo:[GHVRepo new]]; }); // Test the method behaves correctly it(@"has a repo count of one", ^{ [[collection.repos should] haveCountOf:1]; }); }); }); });
  • 26.
    iOS BDD UsingKiwi // Describe how the `GHVCollection` class behaves describe(@"GHVCollection", ^{ // Setup up the collection for each test __block GHVCollection *collection = nil; beforeEach(^{ collection = [GHVCollection new]; }); ! // Describe how the `-addRepo:` method behaves describe(@"-addRepo:", ^{ context(@"after adding a repo", ^{ // Add a repo before each test beforeEach(^{ [collection addRepo:[GHVRepo new]]; }); // Test the method behaves correctly it(@"has a repo count of one", ^{ [[collection.repos should] haveCountOf:1]; }); }); }); });
  • 27.
  • 28.
    Kiwi Benefits • Anunlimited amount of setup and teardown
  • 29.
    Kiwi Benefits • Anunlimited amount of setup and teardown beforeEach(^{ beforeAll(^{ afterEach(^{ afterAll(^{ /* /* /* /* ... ... ... ... */ */ */ */ }); }); }); });
  • 30.
    Kiwi Benefits • Anunlimited amount of setup and teardown beforeEach(^{ beforeAll(^{ afterEach(^{ afterAll(^{ • Mocks and stubs included /* /* /* /* ... ... ... ... */ */ */ */ }); }); }); });
  • 31.
    Kiwi Benefits • Anunlimited amount of setup and teardown beforeEach(^{ beforeAll(^{ afterEach(^{ afterAll(^{ /* /* /* /* ... ... ... ... */ */ */ */ }); }); }); }); • Mocks and stubs included [collection stub:@selector(addRepo:)];
  • 32.
    Kiwi Benefits • Anunlimited amount of setup and teardown beforeEach(^{ beforeAll(^{ afterEach(^{ afterAll(^{ /* /* /* /* ... ... ... ... */ */ */ */ }); }); }); }); • Mocks and stubs included [collection stub:@selector(addRepo:)]; • Asynchronous testing support
  • 33.
    Kiwi Benefits • Anunlimited amount of setup and teardown beforeEach(^{ beforeAll(^{ afterEach(^{ afterAll(^{ /* /* /* /* ... ... ... ... */ */ */ */ }); }); }); }); • Mocks and stubs included [collection stub:@selector(addRepo:)]; • Asynchronous testing support [[collection.repos shouldEventually] haveCountOf:2];
  • 34.
    Kiwi Benefits • Anunlimited amount of setup and teardown beforeEach(^{ beforeAll(^{ afterEach(^{ afterAll(^{ /* /* /* /* ... ... ... ... */ */ */ */ }); }); }); }); • Mocks and stubs included [collection stub:@selector(addRepo:)]; • Asynchronous testing support [[collection.repos shouldEventually] haveCountOf:2]; • More readable than XCTest
  • 35.
  • 36.
    Our First FailingTest /// GHVAPIClientSpec.m ! it(@"gets repositories", ^{ // The repos returned by the API __block NSArray *allRepos = nil; ! // Fetch the repos from the API [client allRepositoriesForUsername:@"modocache" success:^(NSArray *repos) { // Set the repos allRepos = repos; } failure:nil]; ! // Assert that the repos have been set [[expectFutureValue(allRepos) shouldEventually] haveCountOf:10]; });
  • 37.
    Our First FailingTest /// GHVAPIClientSpec.m ! it(@"gets repositories", ^{ // The repos returned by the API __block NSArray *allRepos = nil; ! // Fetch the repos from the API [client allRepositoriesForUsername:@"modocache" success:^(NSArray *repos) { // Set the repos allRepos = repos; } failure:nil]; ! // Assert that the repos have been set [[expectFutureValue(allRepos) shouldEventually] haveCountOf:10]; });
  • 38.
    Our First FailingTest /// GHVAPIClientSpec.m ! it(@"gets repositories", ^{ // The repos returned by the API __block NSArray *allRepos = nil; ! // Fetch the repos from the API [client allRepositoriesForUsername:@"modocache" success:^(NSArray *repos) { // Set the repos allRepos = repos; } failure:nil]; ! // Assert that the repos have been set [[expectFutureValue(allRepos) shouldEventually] haveCountOf:10]; });
  • 39.
    Our First FailingTest /// GHVAPIClientSpec.m ! it(@"gets repositories", ^{ // The repos returned by the API __block NSArray *allRepos = nil; ! // Fetch the repos from the API [client allRepositoriesForUsername:@"modocache" success:^(NSArray *repos) { // Set the repos allRepos = repos; } failure:nil]; ! // Assert that the repos have been set [[expectFutureValue(allRepos) shouldEventually] haveCountOf:10]; });
  • 40.
    Our First FailingTest /// GHVAPIClientSpec.m ! it(@"gets repositories", ^{ // The repos returned by the API __block NSArray *allRepos = nil; ! // Fetch the repos from the API [client allRepositoriesForUsername:@"modocache" success:^(NSArray *repos) { // Set the repos allRepos = repos; } failure:nil]; ! // Assert that the repos have been set [[expectFutureValue(allRepos) shouldEventually] haveCountOf:10]; });
  • 41.
    Our First FailingTest /// GHVAPIClientSpec.m ! it(@"gets repositories", ^{ // The repos returned by the API __block NSArray *allRepos = nil; ! // Fetch the repos from the API [client allRepositoriesForUsername:@"modocache" success:^(NSArray *repos) { // Set the repos allRepos = repos; } failure:nil]; ! // Assert that the repos have been set [[expectFutureValue(allRepos) shouldEventually] haveCountOf:10]; });
  • 42.
  • 43.
    Going Green /// GHVAPIClient.m ! //Create a request operation manager pointing at the GitHub API NSString *urlString = @"https://api.github.com/"; NSURL *baseURL = [NSURL URLWithString:urlString]; AFHTTPRequestOperationManager *manager = [[AFHTTPRequestOperationManager alloc] initWithBaseURL:baseURL]; ! // The manager should serialize the response as JSON manager.requestSerializer = [AFJSONRequestSerializer serializer]; ! // Send a request to GET /users/:username/repos [manager GET:[NSString stringWithFormat:@"users/%@/repos", username] parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) { // Send response object to success block success(responseObject); } failure:nil]; }
  • 44.
    Going Green /// GHVAPIClient.m ! //Create a request operation manager pointing at the GitHub API NSString *urlString = @"https://api.github.com/"; NSURL *baseURL = [NSURL URLWithString:urlString]; AFHTTPRequestOperationManager *manager = [[AFHTTPRequestOperationManager alloc] initWithBaseURL:baseURL]; ! // The manager should serialize the response as JSON manager.requestSerializer = [AFJSONRequestSerializer serializer]; ! // Send a request to GET /users/:username/repos [manager GET:[NSString stringWithFormat:@"users/%@/repos", username] parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) { // Send response object to success block success(responseObject); } failure:nil]; }
  • 45.
    Going Green /// GHVAPIClient.m ! //Create a request operation manager pointing at the GitHub API NSString *urlString = @"https://api.github.com/"; NSURL *baseURL = [NSURL URLWithString:urlString]; AFHTTPRequestOperationManager *manager = [[AFHTTPRequestOperationManager alloc] initWithBaseURL:baseURL]; ! // The manager should serialize the response as JSON manager.requestSerializer = [AFJSONRequestSerializer serializer]; ! // Send a request to GET /users/:username/repos [manager GET:[NSString stringWithFormat:@"users/%@/repos", username] parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) { // Send response object to success block success(responseObject); } failure:nil]; }
  • 46.
    Going Green /// GHVAPIClient.m ! //Create a request operation manager pointing at the GitHub API NSString *urlString = @"https://api.github.com/"; NSURL *baseURL = [NSURL URLWithString:urlString]; AFHTTPRequestOperationManager *manager = [[AFHTTPRequestOperationManager alloc] initWithBaseURL:baseURL]; ! // The manager should serialize the response as JSON manager.requestSerializer = [AFJSONRequestSerializer serializer]; ! // Send a request to GET /users/:username/repos [manager GET:[NSString stringWithFormat:@"users/%@/repos", username] parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) { // Send response object to success block success(responseObject); } failure:nil]; }
  • 47.
    Going Green /// GHVAPIClient.m ! //Create a request operation manager pointing at the GitHub API NSString *urlString = @"https://api.github.com/"; NSURL *baseURL = [NSURL URLWithString:urlString]; AFHTTPRequestOperationManager *manager = [[AFHTTPRequestOperationManager alloc] initWithBaseURL:baseURL]; ! // The manager should serialize the response as JSON manager.requestSerializer = [AFJSONRequestSerializer serializer]; ! // Send a request to GET /users/:username/repos [manager GET:[NSString stringWithFormat:@"users/%@/repos", username] parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) { // Send response object to success block success(responseObject); } failure:nil]; }
  • 48.
    Problems with ourTest • The test has external dependencies • It’ll fail if the GitHub API is down • It’ll fail if run without an internet connection • It’ll fail if the response is too slow • The test is slow • It sends a request every time it’s run
  • 49.
    HTTP Stubbing Eliminating externaldependencies stubRequest(@"GET", @“https://api.github.com/" @“users/modocache/repos") .andReturn(200) .withHeaders(@{@"Content-Type": @"application/json"}) .withBody(@"["repo-1"]"); ! GHVAPIClient *client = [GHVAPIClient new]; ! // ... ! [[expectFutureValue(allRepos) shouldEventually] haveCountOf:1];
  • 50.
    HTTP Stubbing Eliminating externaldependencies stubRequest(@"GET", @“https://api.github.com/" @“users/modocache/repos") .andReturn(200) .withHeaders(@{@"Content-Type": @"application/json"}) .withBody(@"["repo-1"]"); ! GHVAPIClient *client = [GHVAPIClient new]; ! // ... ! [[expectFutureValue(allRepos) shouldEventually] haveCountOf:1];
  • 51.
    HTTP Stubbing Eliminating externaldependencies stubRequest(@"GET", @“https://api.github.com/" @“users/modocache/repos") .andReturn(200) .withHeaders(@{@"Content-Type": @"application/json"}) .withBody(@"["repo-1"]"); ! GHVAPIClient *client = [GHVAPIClient new]; ! // ... ! [[expectFutureValue(allRepos) shouldEventually] haveCountOf:1];
  • 52.
    HTTP Stubbing Eliminating externaldependencies stubRequest(@"GET", @“https://api.github.com/" @“users/modocache/repos") .andReturn(200) .withHeaders(@{@"Content-Type": @"application/json"}) .withBody(@"["repo-1"]"); ! GHVAPIClient *client = [GHVAPIClient new]; ! // ... ! [[expectFutureValue(allRepos) shouldEventually] haveCountOf:1];
  • 53.
    HTTP Stubbing Eliminating externaldependencies stubRequest(@"GET", @“https://api.github.com/" @“users/modocache/repos") .andReturn(200) .withHeaders(@{@"Content-Type": @"application/json"}) .withBody(@"["repo-1"]"); ! GHVAPIClient *client = [GHVAPIClient new]; ! // ... ! [[expectFutureValue(allRepos) shouldEventually] haveCountOf:1];
  • 54.
    HTTP Stubbing Eliminating externaldependencies stubRequest(@"GET", @“https://api.github.com/" @“users/modocache/repos") .andReturn(200) .withHeaders(@{@"Content-Type": @"application/json"}) .withBody(@"["repo-1"]"); ! GHVAPIClient *client = [GHVAPIClient new]; ! // ... ! [[expectFutureValue(allRepos) shouldEventually] haveCountOf:1];
  • 55.
    Problems Nocilla Fixes •The test no longer has external dependencies • It’ll pass whether the GitHub API is online or not • It’ll pass even when run offline • The test is fast • It still sends a request, but that request is immediately intercepted and a response is returned
  • 56.
  • 57.
    Other Nocilla Features •Stub HTTP requests using regular expressions
  • 58.
    Other Nocilla Features •Stub HTTP requests using regular expressions stubRequest(@"GET", @"https://api.github.com/" @"users/(.*?)/repos".regex)
  • 59.
    Other Nocilla Features •Stub HTTP requests using regular expressions stubRequest(@"GET", @"https://api.github.com/" @"users/(.*?)/repos".regex) • Return errors, such as for poor internet connection
  • 60.
    Other Nocilla Features •Stub HTTP requests using regular expressions stubRequest(@"GET", @"https://api.github.com/" @"users/(.*?)/repos".regex) • Return errors, such as for poor internet connection NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:29 userInfo:@{NSLocalizedDescriptionKey: @"Uh-oh!"}]; stubRequest(@"GET", @"...") .andFailWithError(error);
  • 61.
  • 62.
    Takeaways • Readable, behavior-driven,asynchronous tests with Kiwi • https://github.com/allending/Kiwi
  • 63.
    Takeaways • Readable, behavior-driven,asynchronous tests with Kiwi • https://github.com/allending/Kiwi pod "Kiwi/XCTest"
  • 64.
    Takeaways • Readable, behavior-driven,asynchronous tests with Kiwi • https://github.com/allending/Kiwi pod "Kiwi/XCTest" • Eliminate network dependencies with Nocilla • https://github.com/luisobo/Nocilla
  • 65.
    Takeaways • Readable, behavior-driven,asynchronous tests with Kiwi • https://github.com/allending/Kiwi pod "Kiwi/XCTest" • Eliminate network dependencies with Nocilla • https://github.com/luisobo/Nocilla pod "Nocilla"
  • 66.
  • 67.
    Questions? @modocache #startup_ios describe(@"this talk",^{ context(@"after presenting the slides", ^{ it(@"moves to Q&A", ^{ [[you should] askQuestions]; [[you shouldEventually] receive:@selector(stop)]; }); }); });
  • 68.
    Questions? @modocache #startup_ios describe(@"this talk",^{ context(@"after presenting the slides", ^{ it(@"moves to Q&A", ^{ [[you should] askQuestions]; [[you shouldEventually] receive:@selector(stop)]; }); }); });
  • 69.
    Questions? @modocache #startup_ios describe(@"this talk",^{ context(@"after presenting the slides", ^{ it(@"moves to Q&A", ^{ [[you should] askQuestions]; [[you shouldEventually] receive:@selector(stop)]; }); }); });
  • 70.
    Questions? @modocache #startup_ios describe(@"this talk",^{ context(@"after presenting the slides", ^{ it(@"moves to Q&A", ^{ [[you should] askQuestions]; [[you shouldEventually] receive:@selector(stop)]; }); }); });
  • 71.
    Questions? @modocache #startup_ios describe(@"this talk",^{ context(@"after presenting the slides", ^{ it(@"moves to Q&A", ^{ [[you should] askQuestions]; [[you shouldEventually] receive:@selector(stop)]; }); }); });