Share
Embed
Transcript
69 slides
-
Behavior Driven Development in iOS Ian Murray — @synt4x —
github.com/ianmurrays -
Why? • You make sure you don’t break anything in
the future • You can optimize/refactor code knowing it won’t break -
How To Make It Worth The Effort • Don’t write
tests for trivial things -
How To Make It Worth The Effort • Don’t write
tests for trivial things - (NSInteger)add:(NSInteger)this to:(NSInteger)that { return this + that; } -
How To Make It Worth The Effort • Don’t write
tests for trivial things • If you find a bug, write some tests to reproduce it and then fix it -
How To Make It Worth The Effort • Don’t write
tests for trivial things • If you find a bug, write some tests to reproduce it and then fix it • Don’t try and test everything, (especially UI) -
Kiwi • BDD • Simple to install (via CocoaPods) •
Very complete -
Installing $ [sudo] gem install cocoapods $ pod setup $
cd path/to/project $ pod init $ vim Podfile -
Installing $ [sudo] gem install cocoapods $ pod setup $
cd path/to/project $ pod init $ vim Podfile target "BDDKiwi" do ! end ! target "BDDKiwiTests" do pod 'Kiwi/XCTest' # Xcode 5 end -
Installing $ [sudo] gem install cocoapods $ pod setup $
cd path/to/project $ pod init $ vim Podfile target "BDDKiwi" do ! end ! target "BDDKiwiTests" do pod 'Kiwi/XCTest' # Xcode 5 end $ pod install $ open project.xcworkspace -
Our First Spec SPEC_BEGIN(CalculatorSpec) ! describe(@"Calculator", ^{ describe(@"adding up two
numbers", ^{ it(@"should add two numbers", ^{ [[theValue([Calculator add:1 to:1]) should] equal:theValue(2)]; }); }); }); ! ! SPEC_END -
Our First Spec SPEC_BEGIN(CalculatorSpec) ! describe(@"Calculator", ^{ describe(@"adding up two
numbers", ^{ it(@"should add two numbers", ^{ [[theValue([Calculator add:1 to:1]) should] equal:theValue(2)]; }); }); }); ! ! SPEC_END -
Our First Spec SPEC_BEGIN(CalculatorSpec) ! describe(@"Calculator", ^{ describe(@"adding up two
numbers", ^{ it(@"should add two numbers", ^{ [[theValue([Calculator add:1 to:1]) should] equal:theValue(2)]; }); }); }); ! ! SPEC_END -
Our First Spec SPEC_BEGIN(CalculatorSpec) ! describe(@"Calculator", ^{ describe(@"adding up two
numbers", ^{ it(@"should add two numbers", ^{ [[theValue([Calculator add:1 to:1]) should] equal:theValue(2)]; }); }); }); ! ! SPEC_END -
Our First Spec SPEC_BEGIN(CalculatorSpec) ! describe(@"Calculator", ^{ describe(@"adding up two
numbers", ^{ it(@"should add two numbers", ^{ [[theValue([Calculator add:1 to:1]) should] equal:theValue(2)]; }); }); }); ! ! SPEC_END -
Our First Spec SPEC_BEGIN(CalculatorSpec) ! describe(@"Calculator", ^{ describe(@"adding up two
numbers", ^{ it(@"should add two numbers", ^{ [[theValue([Calculator add:1 to:1]) should] equal:theValue(2)]; }); }); }); ! ! SPEC_END -
More Specs describe(@"adding up to NSNumbers", ^{ it(@"should add up
to NSNumbers", ^{ [[[Calculator addNumber:@1 toNumber:@1] should] equal:@2]; }); }); -
More Specs describe(@"adding up to NSNumbers", ^{ it(@"should add up
to NSNumbers", ^{ [[[Calculator addNumber:@1 toNumber:@1] should] equal:@2]; }); }); -
More Specs describe(@"adding up to NSNumbers", ^{ it(@"should add up
to NSNumbers", ^{ [[[Calculator addNumber:@1 toNumber:@1] should] equal:@2]; }); }); -
More Specs describe(@"adding up to NSNumbers", ^{ it(@"should add up
to NSNumbers", ^{ [[[Calculator addNumber:@1 toNumber:@1] should] equal:@2]; }); }); -
More Specs describe(@"adding up to NSNumbers", ^{ it(@"should add up
to NSNumbers", ^{ [[[Calculator addNumber:@1 toNumber:@1] should] equal:@2]; }); }); -
Async Specs describe(@"asynchronously adding NSNumbers", ^{ it(@"should add up two
numbers asynchronously", ^{ __block NSNumber *result; [Calculator addNumber:@1 toNumber:@1 completionHandler:^(NSNumber *_) { result = _; }]; [[expectFutureValue(result) shouldEventually] equal:@2]; }); }); -
Async Specs describe(@"asynchronously adding NSNumbers", ^{ it(@"should add up two
numbers asynchronously", ^{ __block NSNumber *result; [Calculator addNumber:@1 toNumber:@1 completionHandler:^(NSNumber *_) { result = _; }]; [[expectFutureValue(result) shouldEventually] equal:@2]; }); }); -
Async Specs describe(@"asynchronously adding NSNumbers", ^{ it(@"should add up two
numbers asynchronously", ^{ __block NSNumber *result; [Calculator addNumber:@1 toNumber:@1 completionHandler:^(NSNumber *_) { result = _; }]; [[expectFutureValue(result) shouldEventually] equal:@2]; }); }); -
Async Specs describe(@"asynchronously adding NSNumbers", ^{ it(@"should add up two
numbers asynchronously", ^{ __block NSNumber *result; [Calculator addNumber:@1 toNumber:@1 completionHandler:^(NSNumber *_) { result = _; }]; [[expectFutureValue(result) shouldEventually] equal:@2]; }); }); -
Async Specs describe(@"asynchronously adding NSNumbers", ^{ it(@"should add up two
numbers asynchronously", ^{ __block NSNumber *result; [Calculator addNumber:@1 toNumber:@1 completionHandler:^(NSNumber *_) { result = _; }]; [[expectFutureValue(result) shouldEventuallyBeforeTimingOutAfter(10)] equal:@2]; }); }); What if our calculation takes way longer? -
Async Specs describe(@"asynchronously adding NSNumbers", ^{ it(@"should add up two
numbers asynchronously", ^{ __block NSNumber *result; [Calculator addNumber:@1 toNumber:@1 completionHandler:^(NSNumber *_) { result = _; }]; [[expectFutureValue(result) shouldEventuallyBeforeTimingOutAfter(10)] equal:@2]; }); }); What if our calculation takes way longer? -
Async Specs describe(@"asynchronously adding NSNumbers", ^{ it(@"should add up two
numbers asynchronously", ^{ __block NSNumber *result; [Calculator addNumber:@1 toNumber:@1 completionHandler:^(NSNumber *_) { result = _; }]; [[expectFutureValue(result) shouldEventuallyBeforeTimingOutAfter(10)] equal:@2]; }); }); What if our calculation takes way longer? Unfortunately, not an exact science. -
Mocks & Stubs • The Unit in Unit Testing is
important. -
Mocks & Stubs • The Unit in Unit Testing is
important. • You want to test units of code and not your entire app (those would be integration tests). -
Mocks & Stubs • The Unit in Unit Testing is
important. • You want to test units of code and not your entire app (those would be integration tests). • You don’t want your tests to depend on network connectivity • Server responses should remain the same, and server should have its own Unit Tests Suite. -
Mocks & Stubs • The Unit in Unit Testing is
important. • You want to test units of code and not your entire app (those would be integration tests). • You don’t want your tests to depend on network connectivity • Server responses should remain the same, and server should have its own Unit Tests Suite. • You want tests to be fast -
Mocks • Mock objects are “fake” objects that you create
on-the-fly • They do what you configure them to do -
Mocks • Mock objects are “fake” objects that you create
on-the-fly • They do what you configure them to do • Not always useful and normally not necessary -
Stubs • Stubbing is used to switch the functionality of
a method for another -
Stubs • Stubbing is used to switch the functionality of
a method for another • This way you can have an object return what you want, effectively replacing it -
Stubs • Stubbing is used to switch the functionality of
a method for another • This way you can have an object return what you want, effectively replacing it • Useful to set conditions for a given test case -
Stubs • Stubbing is used to switch the functionality of
a method for another • This way you can have an object return what you want, effectively replacing it • Useful to set conditions for a given test case • You can avoid depending on other methods -
typedef NS_ENUM(NSUInteger, CruiserCoreStatus) { CruiserCoreStatusNormal, CruiserCoreStatusAlert, CruiserCoreStatusCritical }; ! //////////////////////////////////////////////////////////////////////
! @interface Cruiser : NSObject ! @property (nonatomic,assign) NSUInteger speed; ! - (NSUInteger)energyLevelForCurrentSpeed; ! /** If energyLevel < 5, then returns CruiserCoreStatusNormal. If 5 <= energyLevel < 10 , then returns CruiserCoreStatusAlert. If 10 <= energyLevel , then returns CruiserCoreStatusCritical. */ - (CruiserCoreStatus)coreStatus; ! @end -
describe(@"coreStatus", ^{ it(@"should return normal for low energy levels", ^{
Cruiser *cruiser = [Cruiser new]; [[cruiser stubAndReturn:theValue(3)] energyLevelForCurrentSpeed]; [[theValue([cruiser coreStatus]) should] equal:theValue(CruiserCoreStatusNormal)]; }); it(@"should return alert for medium energy levels", ^{ Cruiser *cruiser = [Cruiser new]; [cruiser stub:@selector(energyLevelForCurrentSpeed) andReturn:theValue(7)]; [[theValue([cruiser coreStatus]) should] equal:theValue(CruiserCoreStatusAlert)]; }); it(@"should return critical for high energy levels", ^{ Cruiser *cruiser = [Cruiser new]; [cruiser stub:@selector(energyLevelForCurrentSpeed) withBlock:^id(NSArray *params) { return theValue(15); }]; [[theValue([cruiser coreStatus]) should] equal:theValue(CruiserCoreStatusCritical)]; }); }); -
describe(@"coreStatus", ^{ it(@"should return normal for low energy levels", ^{
Cruiser *cruiser = [Cruiser new]; [[cruiser stubAndReturn:theValue(3)] energyLevelForCurrentSpeed]; [[theValue([cruiser coreStatus]) should] equal:theValue(CruiserCoreStatusNormal)]; }); it(@"should return alert for medium energy levels", ^{ Cruiser *cruiser = [Cruiser new]; [cruiser stub:@selector(energyLevelForCurrentSpeed) andReturn:theValue(7)]; [[theValue([cruiser coreStatus]) should] equal:theValue(CruiserCoreStatusAlert)]; }); it(@"should return critical for high energy levels", ^{ Cruiser *cruiser = [Cruiser new]; [cruiser stub:@selector(energyLevelForCurrentSpeed) withBlock:^id(NSArray *params) { return theValue(15); }]; [[theValue([cruiser coreStatus]) should] equal:theValue(CruiserCoreStatusCritical)]; }); }); -
describe(@"coreStatus", ^{ it(@"should return normal for low energy levels", ^{
Cruiser *cruiser = [Cruiser new]; [[cruiser stubAndReturn:theValue(3)] energyLevelForCurrentSpeed]; [[theValue([cruiser coreStatus]) should] equal:theValue(CruiserCoreStatusNormal)]; }); it(@"should return alert for medium energy levels", ^{ Cruiser *cruiser = [Cruiser new]; [cruiser stub:@selector(energyLevelForCurrentSpeed) andReturn:theValue(7)]; [[theValue([cruiser coreStatus]) should] equal:theValue(CruiserCoreStatusAlert)]; }); it(@"should return critical for high energy levels", ^{ Cruiser *cruiser = [Cruiser new]; [cruiser stub:@selector(energyLevelForCurrentSpeed) withBlock:^id(NSArray *params) { return theValue(15); }]; [[theValue([cruiser coreStatus]) should] equal:theValue(CruiserCoreStatusCritical)]; }); }); -
describe(@"coreStatus", ^{ it(@"should return normal for low energy levels", ^{
Cruiser *cruiser = [Cruiser new]; [[cruiser stubAndReturn:theValue(3)] energyLevelForCurrentSpeed]; [[theValue([cruiser coreStatus]) should] equal:theValue(CruiserCoreStatusNormal)]; }); it(@"should return alert for medium energy levels", ^{ Cruiser *cruiser = [Cruiser new]; [cruiser stub:@selector(energyLevelForCurrentSpeed) andReturn:theValue(7)]; [[theValue([cruiser coreStatus]) should] equal:theValue(CruiserCoreStatusAlert)]; }); it(@"should return critical for high energy levels", ^{ Cruiser *cruiser = [Cruiser new]; [cruiser stub:@selector(energyLevelForCurrentSpeed) withBlock:^id(NSArray *params) { return theValue(15); }]; [[theValue([cruiser coreStatus]) should] equal:theValue(CruiserCoreStatusCritical)]; }); }); -
- (void)getLatestResourcesWithCompletionHandler:(void (^)(NSArray *resources))completion { AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
[manager GET:@"http://example.com/resources.json" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) { NSMutableArray *resources = [NSMutableArray array]; // Parse the response for (NSDictionary *resource in responseObject) { Resource *r = [Resource new]; r.name = resource[@"name"]; r.identifier = resource[@"id"]; [resources addObject:r]; } if (completion) completion(resources.copy); } failure:nil]; } -
it(@"should return an array of resources", ^{ NSArray *mockJSON =
@[ @{ @"id": @123, @"name": @"John Doe" }, @{ @"id": @124, @"name": @"Jane Doe" }, ]; // Stub AFNetworking AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager]; [AFHTTPRequestOperationManager stub:@selector(manager) andReturn:manager]; [[AFHTTPRequestOperationManager manager] stub:@selector(GET:parameters:success:failure:) withBlock:^id(NSArray *params) { // Get the success handler void (^success)(id _, id responseObject) = params[2]; // Call it with our JSON success(nil, mockJSON); return nil; // We don't care of what's returned }]; __block NSArray *response = nil; [Network getLatestResourcesWithCompletionHandler:^(NSArray *_) { response = _; }]; [[expectFutureValue([response.firstObject identifier]) shouldEventuallyBeforeTimingOutAfter(10)] equal:@123]; [[expectFutureValue([response.firstObject name]) shouldEventuallyBeforeTimingOutAfter(10)] equal:@"John Doe"]; }); -
it(@"should return an array of resources", ^{ NSArray *mockJSON =
@[ @{ @"id": @123, @"name": @"John Doe" }, @{ @"id": @124, @"name": @"Jane Doe" }, ]; // Stub AFNetworking AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager]; [AFHTTPRequestOperationManager stub:@selector(manager) andReturn:manager]; [[AFHTTPRequestOperationManager manager] stub:@selector(GET:parameters:success:failure:) withBlock:^id(NSArray *params) { // Get the success handler void (^success)(id _, id responseObject) = params[2]; // Call it with our JSON success(nil, mockJSON); return nil; // We don't care of what's returned }]; __block NSArray *response = nil; [Network getLatestResourcesWithCompletionHandler:^(NSArray *_) { response = _; }]; [[expectFutureValue([response.firstObject identifier]) shouldEventuallyBeforeTimingOutAfter(10)] equal:@123]; [[expectFutureValue([response.firstObject name]) shouldEventuallyBeforeTimingOutAfter(10)] equal:@"John Doe"]; }); -
it(@"should return an array of resources", ^{ NSArray *mockJSON =
@[ @{ @"id": @123, @"name": @"John Doe" }, @{ @"id": @124, @"name": @"Jane Doe" }, ]; // Stub AFNetworking AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager]; [AFHTTPRequestOperationManager stub:@selector(manager) andReturn:manager]; [[AFHTTPRequestOperationManager manager] stub:@selector(GET:parameters:success:failure:) withBlock:^id(NSArray *params) { // Get the success handler void (^success)(id _, id responseObject) = params[2]; // Call it with our JSON success(nil, mockJSON); return nil; // We don't care of what's returned }]; __block NSArray *response = nil; [Network getLatestResourcesWithCompletionHandler:^(NSArray *_) { response = _; }]; [[expectFutureValue([response.firstObject identifier]) shouldEventuallyBeforeTimingOutAfter(10)] equal:@123]; [[expectFutureValue([response.firstObject name]) shouldEventuallyBeforeTimingOutAfter(10)] equal:@"John Doe"]; }); -
it(@"should return an array of resources", ^{ NSArray *mockJSON =
@[ @{ @"id": @123, @"name": @"John Doe" }, @{ @"id": @124, @"name": @"Jane Doe" }, ]; // Stub AFNetworking AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager]; [AFHTTPRequestOperationManager stub:@selector(manager) andReturn:manager]; [[AFHTTPRequestOperationManager manager] stub:@selector(GET:parameters:success:failure:) withBlock:^id(NSArray *params) { // Get the success handler void (^success)(id _, id responseObject) = params[2]; // Call it with our JSON success(nil, mockJSON); return nil; // We don't care of what's returned }]; __block NSArray *response = nil; [Network getLatestResourcesWithCompletionHandler:^(NSArray *_) { response = _; }]; [[expectFutureValue([response.firstObject identifier]) shouldEventuallyBeforeTimingOutAfter(10)] equal:@123]; [[expectFutureValue([response.firstObject name]) shouldEventuallyBeforeTimingOutAfter(10)] equal:@"John Doe"]; }); -
it(@"should return an array of resources", ^{ NSArray *mockJSON =
@[ @{ @"id": @123, @"name": @"John Doe" }, @{ @"id": @124, @"name": @"Jane Doe" }, ]; // Stub AFNetworking AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager]; [AFHTTPRequestOperationManager stub:@selector(manager) andReturn:manager]; [[AFHTTPRequestOperationManager manager] stub:@selector(GET:parameters:success:failure:) withBlock:^id(NSArray *params) { // Get the success handler void (^success)(id _, id responseObject) = params[2]; // Call it with our JSON success(nil, mockJSON); return nil; // We don't care of what's returned }]; __block NSArray *response = nil; [Network getLatestResourcesWithCompletionHandler:^(NSArray *_) { response = _; }]; [[expectFutureValue([response.firstObject identifier]) shouldEventuallyBeforeTimingOutAfter(10)] equal:@123]; [[expectFutureValue([response.firstObject name]) shouldEventuallyBeforeTimingOutAfter(10)] equal:@"John Doe"]; });