Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
iOS Unit Testing Workshop
Search
Pete Hodgson
September 17, 2013
Technology
590
3
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
iOS Unit Testing Workshop
Pete Hodgson
September 17, 2013
More Decks by Pete Hodgson
See All by Pete Hodgson
Feature Flags Suck! - KubeCon Atlanta 2025
phodgson
1
300
Migratory Patterns - KubeCon Salt Lake City, 2024
phodgson
0
220
A Journey Into Feature Toggles - OSCON Austin 2017
phodgson
5
730
Test-driven Client-side JS
phodgson
5
930
Functional Reactive JavaScript
phodgson
8
860
different.js - Forward JS 2014
phodgson
4
890
Railsconf2014
phodgson
7
1.5k
Building Your Own Lightsaber
phodgson
104
6.5k
Multi-platform Mobile with Calatrava - May 2013
phodgson
2
580
Other Decks in Technology
See All in Technology
勉強会企画をアプリで構造化してみた 〜そこで見えた、AIとの付き合い方〜 / I've structured a study group plan using an app.
pauli
0
340
実装だけじゃない! CCA-F取得エンジニアが教えるClaude Code開発プロセス活用術
diggymo
2
480
20260702_生成AIはどこまで成長するのか_チャットだけじゃない世界
doradora09
PRO
0
110
Text-to-SQLをAgentCoreで実現し、生成されるSQLの精度を定量的に評価する
yakumo
2
710
AI Driven AI Governance
pict3
0
300
生成AIの活用/high_school2026
okana2ki
0
110
プライバシー保護の理論と実践
lycorptech_jp
PRO
1
250
ローカルLLMとLINE Botの組み合わせ その3 / LINE DC Generative AI Meetup #8
you
PRO
0
130
脱金融のフューチャー・デザイン / Future Design Beyond Finance
ks91
PRO
0
140
ZOZOTOWNの進化と信頼性を両立する負荷試験
zozotech
PRO
1
150
なぜ私たちのSREプラクティスはなかなか機能しないのか 〜システムより先に組織を見る〜 / Why our SRE practices aren't really working
vtryo
3
3.1k
美しいコードを書くためにF#を学んでみた話
yud0uhu
1
380
Featured
See All Featured
Self-Hosted WebAssembly Runtime for Runtime-Neutral Checkpoint/Restore in Edge–Cloud Continuum
chikuwait
0
640
Claude Code どこまでも/ Claude Code Everywhere
nwiizo
65
56k
Gemini Prompt Engineering: Practical Techniques for Tangible AI Outcomes
mfonobong
2
460
The Language of Interfaces
destraynor
162
27k
Evolving SEO for Evolving Search Engines
ryanjones
0
240
ReactJS: Keep Simple. Everything can be a component!
pedronauck
666
130k
Effective software design: The role of men in debugging patriarchy in IT @ Voxxed Days AMS
baasie
0
440
Building Adaptive Systems
keathley
44
3.1k
Information Architects: The Missing Link in Design Systems
soysaucechin
0
1k
SEO for Brand Visibility & Recognition
aleyda
0
4.6k
The Impact of AI in SEO - AI Overviews June 2024 Edition
aleyda
5
1.1k
Organizational Design Perspectives: An Ontology of Organizational Design Elements
kimpetersen
PRO
1
760
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