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
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
320
Migratory Patterns - KubeCon Salt Lake City, 2024
phodgson
0
220
A Journey Into Feature Toggles - OSCON Austin 2017
phodgson
5
740
Test-driven Client-side JS
phodgson
5
940
Functional Reactive JavaScript
phodgson
8
870
different.js - Forward JS 2014
phodgson
4
900
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
500名弱規模の組織のPythonプロジェクト(dbt) をどう管理するか?
hiracky16
0
360
dbt in Microsoft Fabric
ryomaru0825
0
300
Microsoft MVP プログラムを紹介するから目指す人増えてくれ
tsubakimoto_s
0
140
dbt と Snowflake と tag
kevinrobot34
2
290
ハッカソンで入賞した話 @ Findy LT
asari194617
0
1.5k
Bill One 開発エンジニア 紹介資料
sansan33
PRO
7
20k
形式手法特論:Hyperproperty とモデル検査 #kernelvm / Kernel VM Study Tokyo 19th
ytaka23
1
880
Eight Engineering Unit 紹介資料
sansan33
PRO
3
8.3k
名刺メーカーDevグループ 紹介資料
sansan33
PRO
0
1.2k
Oracle AI Databaseデータベース・サービス: BaseDB/ExaDB-Dの可用性
oracle4engineer
PRO
1
950
サービス内で複数のOP・ASを連鎖させる(OAuth/OIDC Numa (Immersion) Workshop 2026)
oidfj
PRO
0
350
NANDでも描画したい!
nichica906
3
810
Featured
See All Featured
The Web Performance Landscape in 2024 [PerfNow 2024]
tammyeverts
12
1.3k
Applied NLP in the Age of Generative AI
inesmontani
PRO
4
2.4k
Become a Pro
speakerdeck
PRO
31
6.2k
Embracing the Ebb and Flow
colly
88
5.1k
A Modern Web Designer's Workflow
chriscoyier
698
190k
Designing Dashboards & Data Visualisations in Web Apps
destraynor
232
55k
Design and Strategy: How to Deal with People Who Don’t "Get" Design
morganepeng
133
19k
Crafting Experiences
bethany
1
270
Have SEOs Ruined the Internet? - User Awareness of SEO in 2025
akashhashmi
0
450
SEO Brein meetup: CTRL+C is not how to scale international SEO
lindahogenes
1
2.8k
The SEO Collaboration Effect
kristinabergwall1
1
530
The Hidden Cost of Media on the Web [PixelPalooza 2025]
tammyeverts
2
480
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