Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Speaker Deck
PRO
Sign in
Sign up for free
iOS Unit Testing Workshop
Pete Hodgson
September 17, 2013
Technology
3
290
iOS Unit Testing Workshop
Pete Hodgson
September 17, 2013
Tweet
Share
More Decks by Pete Hodgson
See All by Pete Hodgson
A Journey Into Feature Toggles - OSCON Austin 2017
phodgson
4
240
Test-driven Client-side JS
phodgson
4
550
Functional Reactive JavaScript
phodgson
8
540
different.js - Forward JS 2014
phodgson
4
550
Railsconf2014
phodgson
7
1.1k
Building Your Own Lightsaber
phodgson
94
4.6k
Multi-platform Mobile with Calatrava - May 2013
phodgson
2
190
Unit testing with Kiwi - CocoaConf San Jose 2013
phodgson
1
330
Automated Mobile Acceptance Testing Presentation - mdevcon 2013
phodgson
2
460
Other Decks in Technology
See All in Technology
Security Hub のマルチアカウント 管理・運用をサーバレスでやってみる
ch6noota
0
820
JJUG2022_spring_Keycloak (Red Hat Single Sign-on)
tinoue
0
200
SwiftUI Layout
auramagi
1
100
MRTK3 - DataBinding and Theming 入門
futo23
0
180
LINEのB2Bプラットフォームにおけるトラブルシューティング2選
line_developers
PRO
3
290
eBPF for Security Observability
lizrice
0
150
Azure Arc Virtual MachineとAzure Arc Resource Bridge / VM provisioning through Azure portal on Azure Stack HCI (preview)
sashizaki
0
130
Modern Android dependency injection
hugovisser
1
130
約6年間運用したシステムをKubernetesに完全移行するまで/Kubernetes Novice Tokyo
isaoshimizu
5
850
Target SDK Versionを上げない Notification runtime permission対応
napplecomputer
0
140
IoTLT88-NTKanazawa-laundry-dry
yukima0707
0
220
データをモデリングしていたら、組織をモデリングし始めた話 / engineers-in-carta-vol3-data-engineer
pei0804
4
3.3k
Featured
See All Featured
ピンチをチャンスに:未来をつくるプロダクトロードマップ #pmconf2020
aki_i
23
15k
Creating an realtime collaboration tool: Agile Flush - .NET Oxford
marcduiker
4
510
Visualizing Your Data: Incorporating Mongo into Loggly Infrastructure
mongodb
29
4.3k
A Philosophy of Restraint
colly
192
15k
Ruby is Unlike a Banana
tanoku
91
9.2k
VelocityConf: Rendering Performance Case Studies
addyosmani
316
22k
Raft: Consensus for Rubyists
vanstee
126
5.4k
GitHub's CSS Performance
jonrohan
1020
420k
StorybookのUI Testing Handbookを読んだ
zakiyama
5
2.2k
Agile that works and the tools we love
rasmusluckow
319
19k
5 minutes of I Can Smell Your CMS
philhawksworth
196
18k
A Modern Web Designer's Workflow
chriscoyier
689
180k
Transcript
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:
me me me Pete Hodgson Consultant at ThoughtWorks @ph1 blog.thepete.net
tell me about yourself experience with iOS? experience with automated
testing? experience testing with iOS?
goals for the workshop hands on experience good testing practices
overview of all the major moving parts of Kiwi introduction to the advanced stuff
some theory, some hands-on (with pairing!) workshop format
interrupt, raise your hand, ask questions Please
why should you care? Unit Testing
None
None
None
1. write code 2. submit stack of cards 3. wait
(probably overnight) 4. find out if your program worked
Feedback
write it check it
feedback
24 hrs punchcards
60 mins big ol’ C++ program
http://xkcd.com/303/
10 secs an iOS app
what’s next?
of feedback speed
of feedback quality
syntax vs. semantics
does it compile? static analysis unit testing
unit testing means better feedback
what is a unit test?
what is NOT unit test?
types of automated testing
Your App End User Backend Services
Unit Test Your App End User Backend Services
Integration Test Your App End User Backend Services
Acceptance Test Your App End User Backend Services
Acceptance Test Your App End User Backend Services The App
Acceptance Unit Integration
“my first unit test” with kiwi
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
setting up a project to use Kiwi
Guide: Up and Running with Kiwi https://github.com/allending/Kiwi/wiki
CocoaPods makes this a lot easier https://github.com/CocoaPods/CocoaPods
what is a unit test?
x+1 12 13
system under test in out
#import "Kiwi.h" SPEC_BEGIN(MyFirstSpec) describe(@"Basic Arithmetic", ^{ it(@"can increment 12",^{ [[theValue(12+1)
should] equal:theValue(13)]; }); }); SPEC_END myFirstSpec.m
Hands-on: writing our first test
$> git checkout sl-2 $> git clean -dxf 1: Close
XCode 2: 3: Open Run2Bart.xcworkspace
#import "Kiwi.h" SPEC_BEGIN(MyFirstSpec) describe(@"Basic Arithmetic", ^{ it(@"can increment 12",^{ [[theValue(12+1)
should] equal:theValue(13)]; }); }); SPEC_END ExampleSpec.m
12+1 does equal 13 does 12*2 equal 24?
#import "Kiwi.h" SPEC_BEGIN(MyFirstSpec) describe(@"Basic Arithmetic", ^{ it(@"can increment 12",^{ [[theValue(12+1)
should] equal:theValue(13)]; }); }); SPEC_END ExampleSpec.m
testing ‘real’ app code parsing JSON
Introducing: Run2Bart
parsing JSON
@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
system under test in out
[ { “name”:”foo”, ... }, {...} ] Station loadStations
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
Arrange, Act, Assert
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
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
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
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"]; }); });
Given, When, Then
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"]; }); });
another simple test displaying estimated departures
5 “5 mins” etdToDisplay
#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
- (NSString *) etdToDisplay{ return [NSString stringWithFormat:@"%@ mins", self.etdInMinutes]; }
UpcomingDeparture.m
5 “5 mins” etdToDisplay
etdToDisplay 1 “1 min” 5 “5 mins”
etdToDisplay 0 “now” 5 “5 mins” 1 “1 min”
Let’s do some TDD!
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
edge-cases made easier
~ BREAK? ~
testing UIKit code
$> git checkout sl-3 $> git clean -dxf 1: Close
XCode 2: 3: Open Run2Bart.xcworkspace
- (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
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
UIKit code is still just code
testing UIKit code rendering UITableViewCells
- (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
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
your turn test that: table view cells have the right
data
hints [departureVC tableView:cellForRowAtIndexPath:] tableViewCell.textLabel.text tableViewCell.detailTextLabel.text BREAK after this
~ BREAK? ~
test doubles (mocks/stubs)
$> git checkout sl-4 $> git clean -dxf 1: Close
XCode 2: 3: Open Run2Bart.xcworkspace
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]; }]; }
Dependencies in Unit Tests
API UI Business Logic
API UI Business Logic SUT
API UI Business Logic SUT
API UI Business Logic SUT
API UI Business Logic SUT TD TD
SUT TD TD
API UI Business Logic SUT
API UI Business Logic SUT View Controller
API UI Business Logic SUT BartClient View Controller
API UI Business Logic SUT TD
API UI Business Logic SUT TD View Controller
API UI Business Logic SUT TD Spy BartClient View Controller
- (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
@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
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
API UI Business Logic SUT TD Spy BartClient View Controller
from hand-rolled to dynamic
@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
UpcomingDeparturesViewControllerSpec.m id mockBartClient = [KWMock mockForProtocol:@protocol(BartClient)];
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]; });
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
verifying what has been sent to a test double
- (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
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]; });
another way to think about mocks/stubs
system under test in out
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
API UI Business Logic SUT BartClient View Controller
API UI Business Logic SUT
usiness ogic SUT SUT TD Test
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
~ BREAK? ~
DRYing up your test code
$> git checkout sl-3 $> git clean -dxf 1: Close
XCode 2: 3: Open Run2Bart.xcworkspace
Don’t Repeat Yourself
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
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
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
your turn: DRY it up UpcomingDeparturesViewControllerSpec.m introduce a beforeEach block
change tests in the file to take advantage of it
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
how unit-testing influences design
None
testable code is also loosely coupled and highly coherent
most of your code shouldn’t be UIKit code
Test-Driven Development
Test-Driven Design
TDD works best with fast tests
extra stuff - xctool - specta/expecta