iOS Unit Testing Workshop

Fe6b81005d1553accd6b2a28f6a2bef1?s=47 Pete Hodgson
September 17, 2013

iOS Unit Testing Workshop

Fe6b81005d1553accd6b2a28f6a2bef1?s=128

Pete Hodgson

September 17, 2013
Tweet

Transcript

  1. Unit Testing for iOS $> git clone --recursive https://github.com/moredip/run2bart-iOS.git $>

    cd run2bart-iOS $> open Run2Bart/Run2Bart.xcworkspace you should have already done this:
  2. me me me Pete Hodgson Consultant at ThoughtWorks @ph1 blog.thepete.net

  3. tell me about yourself experience with iOS? experience with automated

    testing? experience testing with iOS?
  4. goals for the workshop hands on experience good testing practices

    overview of all the major moving parts of Kiwi introduction to the advanced stuff
  5. some theory, some hands-on (with pairing!) workshop format

  6. interrupt, raise your hand, ask questions Please

  7. why should you care? Unit Testing

  8. None
  9. None
  10. None
  11. 1. write code 2. submit stack of cards 3. wait

    (probably overnight) 4. find out if your program worked
  12. Feedback

  13. write it check it

  14. feedback

  15. 24 hrs punchcards

  16. 60 mins big ol’ C++ program

  17. http://xkcd.com/303/

  18. 10 secs an iOS app

  19. what’s next?

  20. of feedback speed

  21. of feedback quality

  22. syntax vs. semantics

  23. does it compile? static analysis unit testing

  24. unit testing means better feedback

  25. what is a unit test?

  26. what is NOT unit test?

  27. types of automated testing

  28. Your App End User Backend Services

  29. Unit Test Your App End User Backend Services

  30. Integration Test Your App End User Backend Services

  31. Acceptance Test Your App End User Backend Services

  32. Acceptance Test Your App End User Backend Services The App

  33. Acceptance Unit Integration

  34. “my first unit test” with kiwi

  35. what is kiwi? open-source tool builds on top of stock

    XCode tooling adds nicer ways of organizing tests adds nicer ways to make assertions adds ability to mock/stub
  36. setting up a project to use Kiwi

  37. Guide: Up and Running with Kiwi https://github.com/allending/Kiwi/wiki

  38. CocoaPods makes this a lot easier https://github.com/CocoaPods/CocoaPods

  39. what is a unit test?

  40. x+1 12 13

  41. system under test in out

  42. #import "Kiwi.h" SPEC_BEGIN(MyFirstSpec) describe(@"Basic Arithmetic", ^{ it(@"can increment 12",^{ [[theValue(12+1)

    should] equal:theValue(13)]; }); }); SPEC_END myFirstSpec.m
  43. Hands-on: writing our first test

  44. $> git checkout sl-2 $> git clean -dxf 1: Close

    XCode 2: 3: Open Run2Bart.xcworkspace
  45. #import "Kiwi.h" SPEC_BEGIN(MyFirstSpec) describe(@"Basic Arithmetic", ^{ it(@"can increment 12",^{ [[theValue(12+1)

    should] equal:theValue(13)]; }); }); SPEC_END ExampleSpec.m
  46. 12+1 does equal 13 does 12*2 equal 24?

  47. #import "Kiwi.h" SPEC_BEGIN(MyFirstSpec) describe(@"Basic Arithmetic", ^{ it(@"can increment 12",^{ [[theValue(12+1)

    should] equal:theValue(13)]; }); }); SPEC_END ExampleSpec.m
  48. testing ‘real’ app code parsing JSON

  49. Introducing: Run2Bart

  50. parsing JSON

  51. @implementation Station + (NSArray *)loadStations:(NSArray *)rawStations { NSMutableArray *stations =

    [NSMutableArray array]; for(NSDictionary *rawStation in rawStations){ Station *station = [[Station alloc] initWithName:rawStation[@"name"] abbr:rawStation[@"abbr"]]; [stations addObject:station]; } return stations; } - (id)initWithName:(NSString *)name abbr:(NSString *)abbr { self = [super init]; if (self) { _name = [name copy]; ! ! _abbr = [abbr copy]; } return self; } @end Station.m
  52. system under test in out

  53. [ { “name”:”foo”, ... }, {...} ] Station loadStations

  54. describe(@"Station", ^{ it(@"loads from JSON correctly",^{ NSArray *rawStations = @[

    @{@"abbr":@"s1",@"name":@"station one"}, @{@"abbr":@"s2",@"name":@"station two"}, ]; NSArray *stations = [Station loadStations:rawStations]; [[theValue(stations.count) should] equal:theValue(2)]; [[[stations[0] abbr] should] equal:@"s1"]; [[[stations[0] name] should] equal:@"station one"]; [[[stations[1] abbr] should] equal:@"s2"]; [[[stations[1] name] should] equal:@"station two"]; }); }); StationSpec.m
  55. Arrange, Act, Assert

  56. describe(@"Station", ^{ it(@"loads from JSON correctly",^{ NSArray *rawStations = @[

    @{@"abbr":@"s1",@"name":@"station one"}, @{@"abbr":@"s2",@"name":@"station two"}, ]; NSArray *stations = [Station loadStations:rawStations]; [[theValue(stations.count) should] equal:theValue(2)]; [[[stations[0] abbr] should] equal:@"s1"]; [[[stations[0] name] should] equal:@"station one"]; [[[stations[1] abbr] should] equal:@"s2"]; [[[stations[1] name] should] equal:@"station two"]; }); }); arrange
  57. describe(@"Station", ^{ it(@"loads from JSON correctly",^{ NSArray *rawStations = @[

    @{@"abbr":@"s1",@"name":@"station one"}, @{@"abbr":@"s2",@"name":@"station two"}, ]; NSArray *stations = [Station loadStations:rawStations]; [[theValue(stations.count) should] equal:theValue(2)]; [[[stations[0] abbr] should] equal:@"s1"]; [[[stations[0] name] should] equal:@"station one"]; [[[stations[1] abbr] should] equal:@"s2"]; [[[stations[1] name] should] equal:@"station two"]; }); }); act
  58. describe(@"Station", ^{ it(@"loads from JSON correctly",^{ NSArray *rawStations = @[

    @{@"abbr":@"s1",@"name":@"station one"}, @{@"abbr":@"s2",@"name":@"station two"}, ]; NSArray *stations = [Station loadStations:rawStations]; [[theValue(stations.count) should] equal:theValue(2)]; [[[stations[0] abbr] should] equal:@"s1"]; [[[stations[0] name] should] equal:@"station one"]; [[[stations[1] abbr] should] equal:@"s2"]; [[[stations[1] name] should] equal:@"station two"]; }); }); assert
  59. describe(@"Station", ^{ it(@"loads from JSON correctly",^{ ////////////////// // Arrange NSArray

    *rawStations = @[ @{@"abbr":@"s1",@"name":@"station one"}, @{@"abbr":@"s2",@"name":@"station two"}, ]; ////////////////// // Act NSArray *stations = [Station loadStations:rawStations]; ////////////////// // Assert [[theValue(stations.count) should] equal:theValue(2)]; [[[stations[0] abbr] should] equal:@"s1"]; [[[stations[0] name] should] equal:@"station one"]; [[[stations[1] abbr] should] equal:@"s2"]; [[[stations[1] name] should] equal:@"station two"]; }); });
  60. Given, When, Then

  61. describe(@"Station", ^{ it(@"loads from JSON correctly",^{ ////////////////// // Given NSArray

    *rawStations = @[ @{@"abbr":@"s1",@"name":@"station one"}, @{@"abbr":@"s2",@"name":@"station two"}, ]; ////////////////// // When NSArray *stations = [Station loadStations:rawStations]; ////////////////// // Then [[theValue(stations.count) should] equal:theValue(2)]; [[[stations[0] abbr] should] equal:@"s1"]; [[[stations[0] name] should] equal:@"station one"]; [[[stations[1] abbr] should] equal:@"s2"]; [[[stations[1] name] should] equal:@"station two"]; }); });
  62. another simple test displaying estimated departures

  63. 5 “5 mins” etdToDisplay

  64. #import "Kiwi.h" #import "UpcomingDeparture.h" SPEC_BEGIN(UpcomingDepartureSpec) describe(@"[UpcomingDeparture etdToDisplay]", ^{ it(@"is correct

    for an etd of 5",^{ UpcomingDeparture *upcomingDeparture = [[UpcomingDeparture alloc] initWithDestinationName:@"blah" etd:@(5)]; [[[upcomingDeparture etdToDisplay] should] equal:@"5 mins"]; }); }); SPEC_END UpcomingDepartureSpec.m
  65. - (NSString *) etdToDisplay{ return [NSString stringWithFormat:@"%@ mins", self.etdInMinutes]; }

    UpcomingDeparture.m
  66. 5 “5 mins” etdToDisplay

  67. etdToDisplay 1 “1 min” 5 “5 mins”

  68. etdToDisplay 0 “now” 5 “5 mins” 1 “1 min”

  69. Let’s do some TDD!

  70. describe(@"[UpcomingDeparture etdToDisplay]", ^{ it(@"is correct for an etd of 5",^{

    UpcomingDeparture *upcomingDeparture = [[UpcomingDeparture alloc] initWithDestinationName:@"blah" etd:@(5)]; [[[upcomingDeparture etdToDisplay] should] equal:@"5 mins"]; }); it(@"is correct for an etd of 1",^{ UpcomingDeparture *upcomingDeparture = [[UpcomingDeparture alloc] initWithDestinationName:@"blah" etd:@(1)]; [[[upcomingDeparture etdToDisplay] should] equal:@"1 min"]; }); it(@"is correct for an etd of 0",^{ UpcomingDeparture *upcomingDeparture = [[UpcomingDeparture alloc] initWithDestinationName:@"blah" etd:@(0)]; [[[upcomingDeparture etdToDisplay] should] equal:@"now"]; }); }); UpcomingDepartureSpec.m
  71. edge-cases made easier

  72. ~ BREAK? ~

  73. testing UIKit code

  74. $> git checkout sl-3 $> git clean -dxf 1: Close

    XCode 2: 3: Open Run2Bart.xcworkspace
  75. - (id)initForStation:(Station *)station { self = [super init]; if (self)

    { self.station = station; self.title = station.name; self.bartClient = [AppDelegate sharedInstance].bartClient; } return self; } UpcomingDeparturesViewController.m
  76. SPEC_BEGIN(UpcomingDeparturesViewControllerSpec) describe( @"UpcomingDeparturesViewController ", ^{ it( @"has the station's name

    as the title", ^{ // Given Station *theStation = [[Station alloc] initWithName:@"station name" abbr:@"station-abbr"]; UpcomingDeparturesViewController *theDeparturesVC = [[UpcomingDeparturesViewController alloc] initForStation:theStation]; // Then [[theDeparturesVC.title should] equal:@"station name"]; }); // ... UpcomingDeparturesViewControllerSpec.m
  77. UIKit code is still just code

  78. testing UIKit code rendering UITableViewCells

  79. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier

    = @"Cell"; UpcomingDeparture *departure = [self.departures objectAtIndex:indexPath.row]; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if( !cell ) cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:CellIdentifier]; cell.textLabel.font = [UIFont systemFontOfSize:18.0]; cell.detailTextLabel.font = [UIFont systemFontOfSize:24.0]; cell.textLabel.text = departure.destinationName; cell.detailTextLabel.text = departure.etdToDisplay; return cell; } UpcomingDeparturesViewController.m
  80. describe(@"rendering departures", ^{ it( @"renders a table view cell for

    each departure", ^{ // Given UITableView *ignoredTableView = nil; Station *someStation = [[Station alloc] initWithName:@"station name" abbr:@"station-abbr"]; UpcomingDeparturesViewController *departuresVC = [[UpcomingDeparturesViewController alloc] initForStation:someStation]; NSArray *departures = @[ [[UpcomingDeparture alloc] initWithDestinationName:@"dest 1" etd:@(0)], [[UpcomingDeparture alloc] initWithDestinationName:@"dest 2" etd:@(1)], [[UpcomingDeparture alloc] initWithDestinationName:@"dest 3" etd:@(2)] ]; departuresVC.departures = departures; // When NSInteger numSections = [departuresVC numberOfSectionsInTableView:ignoredTableView]; NSInteger numRows = [departuresVC tableView:ignoredTableView numberOfRowsInSection:0]; // Then [[theValue(numSections) should] equal:theValue(1)]; [[theValue(numRows) should] equal:theValue(3)]; }); }); UpcomingDeparturesViewControllerSpec.m
  81. your turn test that: table view cells have the right

    data
  82. hints [departureVC tableView:cellForRowAtIndexPath:] tableViewCell.textLabel.text tableViewCell.detailTextLabel.text BREAK after this

  83. ~ BREAK? ~

  84. test doubles (mocks/stubs)

  85. $> git checkout sl-4 $> git clean -dxf 1: Close

    XCode 2: 3: Open Run2Bart.xcworkspace
  86. UpcomingDeparturesViewController.m - (void)viewWillAppear:(BOOL)animated{ [self refreshUpcomingDepartures]; } - (void) refreshUpcomingDepartures{ [self.refreshControl

    beginRefreshing]; [self.bartClient fetchUpcomingDeparturesForStation:self.station success:^(NSArray *departures) { [self.refreshControl endRefreshing]; self.departures = departures; [self.tableView reloadData]; } failure:^(NSError *error) { [self.refreshControl endRefreshing]; }]; }
  87. Dependencies in Unit Tests

  88. API UI Business Logic

  89. API UI Business Logic SUT

  90. API UI Business Logic SUT

  91. API UI Business Logic SUT

  92. API UI Business Logic SUT TD TD

  93. SUT TD TD

  94. API UI Business Logic SUT

  95. API UI Business Logic SUT View Controller

  96. API UI Business Logic SUT BartClient View Controller

  97. API UI Business Logic SUT TD

  98. API UI Business Logic SUT TD View Controller

  99. API UI Business Logic SUT TD Spy BartClient View Controller

  100. - (void)viewWillAppear:(BOOL)animated{ [self refreshUpcomingDepartures]; } - (void) refreshUpcomingDepartures{ [self.refreshControl beginRefreshing];

    [self.bartClient fetchUpcomingDeparturesForStation:self.station success:^(NSArray *departures) { [self.refreshControl endRefreshing]; self.departures = departures; [self.tableView reloadData]; } failure:^(NSError *error) { [self.refreshControl endRefreshing]; }]; } UpcomingDeparturesViewController.m
  101. @interface SpyBartClient : NSObject<BartClient>{ BOOL _fetchWasCalled; } @end @implementation SpyBartClient

    - (id)init { self = [super init]; if (self) { _fetchWasCalled = NO; } return self; } - (void)fetchUpcomingDeparturesForStation:(Station *)station success:(FetchSuccessBlock)success failure:(FetchFailureBlock)failure{ _fetchWasCalled = YES; } - (BOOL)fetchUpcomingDeparturesWasCalled{ return _fetchWasCalled; } @end UpcomingDeparturesViewControllerSpec.m
  102. describe(@"the VC's view appearing", ^{ it( @"asks the api client

    to load the upcoming departures", ^{ SpyBartClient *testDoubleBartClient = [[SpyBartClient alloc] init]; departuresVC.bartClient = testDoubleBartClient; [departuresVC viewWillAppear:YES]; [[theValue([testDoubleBartClient fetchUpcomingDeparturesWasCalled]) should] beTrue]; }); }); UpcomingDeparturesViewControllerSpec.m
  103. API UI Business Logic SUT TD Spy BartClient View Controller

  104. from hand-rolled to dynamic

  105. @interface SpyBartClient : NSObject<BartClient>{ BOOL _fetchWasCalled; } @end @implementation SpyBartClient

    - (id)init { self = [super init]; if (self) { _fetchWasCalled = NO; } return self; } - (void)fetchUpcomingDeparturesForStation:(Station *)station success:(FetchSuccessBlock)success failure:(FetchFailureBlock)failure{ _fetchWasCalled = YES; } - (BOOL)fetchUpcomingDeparturesWasCalled{ return _fetchWasCalled; } @end UpcomingDeparturesViewControllerSpec.m
  106. UpcomingDeparturesViewControllerSpec.m id mockBartClient = [KWMock mockForProtocol:@protocol(BartClient)];

  107. UpcomingDeparturesViewControllerSpec.m it( @"asks the api client to load the upcoming

    departures", ^{ id mockBartClient = [KWMock mockForProtocol:@protocol(BartClient)]; departuresVC.bartClient = mockBartClient; [[mockBartClient should] receive:@selector(fetchUpcomingDeparturesForStation:success:failure:) andReturn:nil]; [departuresVC viewWillAppear:YES]; });
  108. it( @"asks the api client to load the upcoming departures",

    ^{ id mockBartClient = [KWMock mockForProtocol:@protocol(BartClient)]; departuresVC.bartClient = mockBartClient; [[mockBartClient should] receive:@selector(fetchUpcomingDeparturesForStation:success:failure:) andReturn:nil]; [departuresVC viewWillAppear:YES]; }); it( @"asks the api client to load the upcoming departures", ^{ SpyBartClient *testDoubleBartClient = [[SpyBartClient alloc] init]; departuresVC.bartClient = testDoubleBartClient; [departuresVC viewWillAppear:YES]; [[theValue([testDoubleBartClient fetchUpcomingDeparturesWasCalled] should] beTrue]; }); hand-rolled dynamic
  109. verifying what has been sent to a test double

  110. - (void)viewWillAppear:(BOOL)animated{ [self refreshUpcomingDepartures]; } - (void) refreshUpcomingDepartures{ [self.refreshControl beginRefreshing];

    [self.bartClient fetchUpcomingDeparturesForStation:self.station success:^(NSArray *departures) { [self.refreshControl endRefreshing]; self.departures = departures; [self.tableView reloadData]; } failure:^(NSError *error) { [self.refreshControl endRefreshing]; }]; } UpcomingDeparturesViewController.m
  111. UpcomingDeparturesViewControllerSpec.m it( @"asks the api client to load the upcoming

    departures for the right station", ^{ id mockBartClient = [KWMock mockForProtocol:@protocol(BartClient)]; departuresVC.bartClient = mockBartClient; [[[mockBartClient should] receive] fetchUpcomingDeparturesForStation:station success:any() failure:any()]; [departuresVC viewWillAppear:YES]; });
  112. another way to think about mocks/stubs

  113. system under test in out

  114. D e p e n d e n c y

    system under test indirect output indirect input direct input direct output T e s t
  115. API UI Business Logic SUT BartClient View Controller

  116. API UI Business Logic SUT

  117. usiness ogic SUT SUT TD Test

  118. D e p e n d e n c y

    system under test indirect output indirect input direct input direct output T e s t values passed to dependency via method call values returned from call to dependency
  119. ~ BREAK? ~

  120. DRYing up your test code

  121. $> git checkout sl-3 $> git clean -dxf 1: Close

    XCode 2: 3: Open Run2Bart.xcworkspace
  122. Don’t Repeat Yourself

  123. describe( @"UpcomingDeparturesViewController ", ^{ it( @"has the station's name as

    the title", ^{ // Given Station *theStation = [[Station alloc] initWithName:@"station name" abbr:@"station-abbr"]; UpcomingDeparturesViewController *theDeparturesVC = [[UpcomingDeparturesViewController alloc] initForStation:theStation]; // Then [[theDeparturesVC.title should] equal:@"station name"]; }); // ... UpcomingDeparturesViewControllerSpec.m
  124. describe( @"UpcomingDeparturesViewController ", ^{ it( @"has the station's name as

    the title", ^{ // Given Station *theStation = [[Station alloc] initWithName:@"station name" abbr:@"station-abbr"]; UpcomingDeparturesViewController *theDeparturesVC = [[UpcomingDeparturesViewController alloc] initForStation:theStation]; // Then [[theDeparturesVC.title should] equal:@"station name"]; }); // ... UpcomingDeparturesViewControllerSpec.m
  125. describe( @"UpcomingDeparturesViewController ", ^{ __block Station *station; __block UpcomingDeparturesViewController *departuresVC;

    beforeEach(^{ station = [[Station alloc] initWithName:@"station name" abbr:@"station-abbr"]; departuresVC = [[UpcomingDeparturesViewController alloc] initForStation:station]; }) it( @"has the station's name as the title", ^{ // Then [[departuresVC.title should] equal:station.name]; }); // ... UpcomingDeparturesViewControllerSpec.m
  126. your turn: DRY it up UpcomingDeparturesViewControllerSpec.m introduce a beforeEach block

    change tests in the file to take advantage of it
  127. describe( @"UpcomingDeparturesViewController ", ^{ __block Station *station; __block UpcomingDeparturesViewController *departuresVC;

    beforeEach(^{ station = [[Station alloc] initWithName:@"station name" abbr:@"station-abbr"]; departuresVC = [[UpcomingDeparturesViewController alloc] initForStation:station]; }) it( @"has the station's name as the title", ^{ // Then [[departuresVC.title should] equal:station.name]; }); // ... UpcomingDeparturesViewControllerSpec.m
  128. how unit-testing influences design

  129. None
  130. testable code is also loosely coupled and highly coherent

  131. most of your code shouldn’t be UIKit code

  132. Test-Driven Development

  133. Test-Driven Design

  134. TDD works best with fast tests

  135. extra stuff - xctool - specta/expecta