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
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
13. 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
14. 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
15. 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
16. 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
17. 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
18. 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
21. 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];
});
});
});
});
22. 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];
});
});
});
});
23. 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];
});
});
});
});
24. 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];
});
});
});
});
25. 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];
});
});
});
});
26. 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];
});
});
});
});
36. 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];
});
37. 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];
});
38. 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];
});
39. 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];
});
40. 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];
});
41. 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];
});
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 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
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
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);