Upgrade to Pro — share decks privately, control downloads, hide ads and more …

BDD on iOS with Kiwi

Ian Murray
February 27, 2014

BDD on iOS with Kiwi

Ian Murray

February 27, 2014
Tweet

More Decks by Ian Murray

Other Decks in Programming

Transcript

  1. Why? • You make sure you don’t break anything in

    the future • You can optimize/refactor code knowing it won’t break
  2. How To Make It Worth The Effort • Don’t write

    tests for trivial things - (NSInteger)add:(NSInteger)this to:(NSInteger)that { return this + that; }
  3. 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
  4. 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)
  5. Installing $ [sudo] gem install cocoapods $ pod setup $

    cd path/to/project $ pod init $ vim Podfile
  6. 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
  7. 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
  8. 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
  9. 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
  10. 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
  11. 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
  12. 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
  13. 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
  14. More Specs describe(@"adding up to NSNumbers", ^{ it(@"should add up

    to NSNumbers", ^{ [[[Calculator addNumber:@1 toNumber:@1] should] equal:@2]; }); });
  15. More Specs describe(@"adding up to NSNumbers", ^{ it(@"should add up

    to NSNumbers", ^{ [[[Calculator addNumber:@1 toNumber:@1] should] equal:@2]; }); });
  16. More Specs describe(@"adding up to NSNumbers", ^{ it(@"should add up

    to NSNumbers", ^{ [[[Calculator addNumber:@1 toNumber:@1] should] equal:@2]; }); });
  17. More Specs describe(@"adding up to NSNumbers", ^{ it(@"should add up

    to NSNumbers", ^{ [[[Calculator addNumber:@1 toNumber:@1] should] equal:@2]; }); });
  18. More Specs describe(@"adding up to NSNumbers", ^{ it(@"should add up

    to NSNumbers", ^{ [[[Calculator addNumber:@1 toNumber:@1] should] equal:@2]; }); });
  19. 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]; }); });
  20. 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]; }); });
  21. 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]; }); });
  22. 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]; }); });
  23. 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?
  24. 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?
  25. 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.
  26. 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).
  27. 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.
  28. 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
  29. Mocks • Mock objects are “fake” objects that you create

    on-the-fly • They do what you configure them to do
  30. 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
  31. 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
  32. 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
  33. 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
  34. 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
  35. 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)]; }); });
  36. 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)]; }); });
  37. 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)]; }); });
  38. 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)]; }); });
  39. - (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]; }
  40. 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"]; });
  41. 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"]; });
  42. 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"]; });
  43. 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"]; });
  44. 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"]; });