Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
iOS Unit Testing Workshop
Search
Sponsored
·
Ship Features Fearlessly
Turn features on and off without deploys. Used by thousands of Ruby developers.
→
Pete Hodgson
September 17, 2013
Technology
600
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
340
Migratory Patterns - KubeCon Salt Lake City, 2024
phodgson
0
240
A Journey Into Feature Toggles - OSCON Austin 2017
phodgson
5
750
Test-driven Client-side JS
phodgson
5
940
Functional Reactive JavaScript
phodgson
8
880
different.js - Forward JS 2014
phodgson
4
910
Railsconf2014
phodgson
7
1.5k
Building Your Own Lightsaber
phodgson
104
6.5k
Multi-platform Mobile with Calatrava - May 2013
phodgson
2
590
Other Decks in Technology
See All in Technology
顧客価値を追いかけたら、プライシングにたどり着いた
sansantech
PRO
2
330
Kiro Crewで「みのるん式」仕事術やってみた
minorun365
PRO
3
980
CloudWatch Logsのコスト最適化:どこまでAIに任せてどこから人間が承認すべきか
yama3133
0
150
Claude Code on Bedrock で統制をかける小ネタ集/claude-code-bedrock-governance-tips
emiki
0
110
PagerDuty Tech Hub-鳴らすけれど、人は動かない
ezaki
0
190
DBについての基礎説明(MySQL編) - コロプラ 2026年度 新卒研修
colopl
0
160
AI駆動開発におけるハーネスエンジニアリングの勘所
tsukuboshi
1
200
古い指輪から広がった JAVA コミュニティ とのつながり
yamad
0
130
What Is JetBrains Air
x5gtrn
PRO
0
130
AI駆動で「他アプリの音を加工できる」アプリを作った
satomasahiro2005
0
190
sue445's proposals 2026
sue445
0
120
「エーアイ」をバズワードにしないために RPA 導入経験と Kiro 上での Claude モデル利用経験から
kazzpapa3
0
110
Featured
See All Featured
Mind Mapping
helmedeiros
1
400
Thoughts on Productivity
jonyablonski
76
5.4k
Embracing the Ebb and Flow
colly
88
5.2k
Mobile First: as difficult as doing things right
swwweet
225
10k
Money Talks: Using Revenue to Get Sh*t Done
nikkihalliwell
0
530
From Legacy to Launchpad: Building Startup-Ready Communities
dugsong
0
360
WCS-LA-2024
lcolladotor
0
900
Building Experiences: Design Systems, User Experience, and Full Site Editing
marktimemedia
1
640
Information Architects: The Missing Link in Design Systems
soysaucechin
1
1.2k
Writing Fast Ruby
sferik
630
63k
Odyssey Design
rkendrick25
PRO
2
880
The Organizational Zoo: Understanding Human Behavior Agility Through Metaphoric Constructive Conversations (based on the works of Arthur Shelley, Ph.D)
kimpetersen
PRO
0
500
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