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
Sponsored
·
Your Podcast. Everywhere. Effortlessly.
Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
→
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
310
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
930
Functional Reactive JavaScript
phodgson
8
870
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
590
Other Decks in Technology
See All in Technology
Software Supply Chain Attackからクラウド環境を守るためにできること
lhazy
2
220
20260801_スクフェス大阪
kgnkhkr
2
1.2k
TypeScript入門 2026
recruitengineers
PRO
2
490
SmartHR Engineering Team Deck
smarthr
1
990
ガバメントクラウドでのランサムウェア対策
techniczna
2
990
Eight Engineering Unit 紹介資料
sansan33
PRO
3
8.1k
もう一度考える SRE チームの作り方・育て方 / Rethinking SRE #1: Building and Growing SRE Teams
rrreeeyyy
6
1k
え?フロントエンドエンジニアの ワイがインフラも!?
puku0x
1
430
つくって納得、つかって実感! 大規模言語モデルことはじめ ver2.0
recruitengineers
PRO
3
1.2k
ボトムアップ文化が強い組織で セキュリティをどう根付かせていくかの現在進行形の話 / Making Security Stick in a Bottom-Up Organization
yamaguchitk333
0
200
20260608_Codexの可能性_ノンプログラマー向け_大城追記
doradora09
PRO
0
780
第3回しろおびセキュリティスポンサーセッション
log0417
0
160
Featured
See All Featured
職位にかかわらず全員がリーダーシップを発揮するチーム作り / Building a team where everyone can demonstrate leadership regardless of position
madoxten
64
56k
Designing for Performance
lara
611
70k
Become a Pro
speakerdeck
PRO
31
6.2k
Cheating the UX When There Is Nothing More to Optimize - PixelPioneers
stephaniewalter
287
14k
Rails Girls Zürich Keynote
gr2m
96
14k
JAMstack: Web Apps at Ludicrous Speed - All Things Open 2022
reverentgeek
1
540
Exploring anti-patterns in Rails
aemeredith
3
460
Producing Creativity
orderedlist
PRO
348
40k
Making Projects Easy
brettharned
120
6.7k
Prompt Engineering for Job Search
mfonobong
0
400
Between Models and Reality
mayunak
4
390
The Straight Up "How To Draw Better" Workshop
denniskardys
239
140k
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