Slide 1

Slide 1 text

MASTERING TDD FOR iOS

Slide 2

Slide 2 text

” “ — Andy Hunt, David Thomas, The Pragmatic Programmer LIKE OUR HARDWARE COLLEAGUES, WE NEED TO BUILD TESTABILITY INTO SOFTWARE FROM THE VERY BEGINNING

Slide 3

Slide 3 text

TOTAL COST OF OWNING A MESS PRODUCTIVITY 0 25 50 75 100 TIME Source: Clean Code, Robert C. Martin

Slide 4

Slide 4 text

THE THREE LAWS OF TDD

Slide 5

Slide 5 text

” “ — First Law of TDD YOU MAY NOT WRITE PRODUCTION CODE UNTIL YOU HAVE WRITTEN A FAILING TEST

Slide 6

Slide 6 text

” “ — Second Law of TDD YOU MAY NOT WRITE MORE OF A UNIT TEST THAN IS SUFFICIENT TO FAIL AND NOT COMPILING IS FAILING

Slide 7

Slide 7 text

” “ — Second Law of TDD YOU MAY NOT WRITE MORE PRODUCTION CODE THAN IS SUFFICIENT TO PASS THE CURRENTLY FAILING TEST

Slide 8

Slide 8 text

WORKING EFFECTIVELY WITH LEGACY CODE *Working effectively with legacy code, Michael C. Feathers • Write a failing test • Get it to compile • Make it pass • Remove Duplication • Repeat TDD

Slide 9

Slide 9 text

CLEARING THE THORNS CALCULATOR Go to Test Driven Development Xcode What happens with Zero elements, NaN what else could now happen down the line.

Slide 10

Slide 10 text

” “ — Robert C. Martin, Thorns around the Gold, Clean Coder Blog SHY AWAY FROM ANY TESTS THAT ARE CLOSE TO THE CORE FUNCTIONALITY UNTIL I HAVE COMPLETELY SURROUNDED THE PROBLEM WITH PASSING TESTS THAT DESCRIBE EVERYTHING BUT THE CORE FUNCTIONALITY blog.cleancoder.com/uncle-bob/2014/11/19/GoingForTheGold.html

Slide 11

Slide 11 text

” “ — Robert Martin, Clean Code. READABILITY, READABILITY, READABILITY

Slide 12

Slide 12 text

func testGetDataAsHtml(){ crawler.addPage(root, PathParser.parse("TestPageOne"), "test page") request.setResource("TestPageOne") request.addInput("type", "data") let responder = SerializedPageResponder() let response = responder.makeResponse(Context(root), request) let xml = response.getContent() XCTAssertEquals("text/xml", response.getContentType()) assertSubString("test page", xml) assertSubString("PageOne", xml) assertSubString("PageTwo", xml) assertSubString("ChildOne", xml) } func testGetPageHieratchyAsXmlDoesntContainSymbolicLinks() { let pageOne = crawler.addPage(root, PathParser.parse("PageOne")) crawler.addPage(root, PathParser.parse("PageOne.ChildOne")) crawler.addPage(root, PathParser.parse("PageTwo")) let data = pageOne.getData() let properties = data.getProperties() let symLinks = properties.set(SymbolicPage.PROPERTY_NAME) symLinks.set("SymPage", "PageTwo") pageOne.commit(data) request.setResource("root") request.addInput("type", "pages") let responder = SerializedPageResponder() let response = responder.makeResponse(Context(root), request) as? SimpleResponse let xml = response.getContent() XCTAssertEqual("text/xml", response.getContentType()) assertSubString("PageOne", xml) assertSubString("PageTwo", xml) assertSubString("ChildOne", xml) assertNotSubString("SymPage", xml) }

Slide 13

Slide 13 text

•BUILD •OPERATE •CHECK ARRANGE ACT ASSERT

Slide 14

Slide 14 text

{P}C{Q} Precondition Post condition Command HOARE’S TRIPLE This was established in 1969!

Slide 15

Slide 15 text

func testGetDataAsHtml(){ crawler.addPage(root, PathParser.parse("TestPageOne"), "test page") request.setResource("TestPageOne") request.addInput("type", "data") let responder = SerializedPageResponder() let response = responder.makeResponse(Context(root), request) let xml = response.getContent() XCTAssertEquals("text/xml", response.getContentType()) assertSubString("test page", xml) assertSubString("PageOne", xml) assertSubString("PageTwo", xml) assertSubString("ChildOne", xml) } func testGetPageHieratchyAsXmlDoesntContainSymbolicLinks() { let pageOne = crawler.addPage(root, PathParser.parse("PageOne")) crawler.addPage(root, PathParser.parse("PageOne.ChildOne")) crawler.addPage(root, PathParser.parse("PageTwo")) let data = pageOne.getData() let properties = data.getProperties() let symLinks = properties.set(SymbolicPage.PROPERTY_NAME) symLinks.set("SymPage", "PageTwo") pageOne.commit(data) request.setResource("root") request.addInput("type", "pages") let responder = SerializedPageResponder() let response = responder.makeResponse(Context(root), request) as? SimpleResponse let xml = response.getContent() XCTAssertEqual("text/xml", response.getContentType()) assertSubString("PageOne", xml) assertSubString("PageTwo", xml) assertSubString("ChildOne", xml) assertNotSubString("SymPage", xml) } //SETUP //ACTION //ASSERT //SETUP //ACTION //ASSERT

Slide 16

Slide 16 text

func testGetPageHierarchyAsXml() { makePages("PageOne", "PageOne.ChildOne", "PageTwo") submitRequest("root", "type:pages") assertResponseIsXML() assertResponseContains("PageOne", "PageTwo", "ChildOne") } func testSymbolicLinksAreNotInXmlPageHierarchy() { let page = makePage("PageOne") makePages("PageOne.ChildOne", "PageTwo") addLinkTo(page, "PageTwo", "SymPage") submitRequest("root", "type:pages") assertResponseIsXML() assertResponseContains("PageOne", "PageTwo","ChildOne") assertResponseDoesNotContain("SymPage") } func testGetDataAsXml() { makePageWithContent("TestPageOne", "test page") submitRequest("TestPageOne", "type:data") assertResponseIsXML() assertResponseContains("test page", "

Slide 17

Slide 17 text

func testGetPageHierarchyAsXml() { makePages("PageOne", "PageOne.ChildOne", "PageTwo") submitRequest("root", "type:pages") assertXMLResponseContains("PageOne", "PageTwo", "ChildOne") } func testSymbolicLinksAreNotInXmlPageHierarchy() { let page = makePage("PageOne") makePages("PageOne.ChildOne", "PageTwo") addLinkTo(page, "PageTwo", "SymPage") submitRequest("root", "type:pages") assertXMLResponseContains("PageOne", "PageTwo","ChildOne") assertXMLResponseDoesNotContain("SymPage") } func testGetDataAsXml() { makePageWithContent("TestPageOne", "test page") submitRequest("TestPageOne", "type:data") assertXMLResponseContains("test page", "

Slide 18

Slide 18 text

- CLEAN CODE “CLEAN TESTS FOLLOW THESE FIVE RULES”. • F.AST • I.NDEPENDANT • R.EPEATABLE • S.ELF-VALIDATING • T.IMELY

Slide 19

Slide 19 text

MOCKING

Slide 20

Slide 20 text

CLASSIC MOCKIST

Slide 21

Slide 21 text

ROY OSHEROVE, THE ART OF UNIT TESTING ASSERTIONS • Check the return value, or an exception. (Classic) • Check the state of the object, or the state of a collaborator. (Classic) • Check the object correctly interacts with a collaborator. (Mockist)

Slide 22

Slide 22 text

ANY KIND OF PRETEND OBJECT USED IN PLACE OF A REAL OBJECT FOR TESTING PURPOSES TEST DOUBLES • Dummy • Fake • Stubs, • Spys • Mocks Behaviour Verification State Verification BDD Classic TDD Outcomes Mechanisms Low Coupling High Coupling Martin Fowler, Mocks aren’t Stubs - http://martinfowler.com/articles/mocksArentStubs.html

Slide 23

Slide 23 text

http://coding-is-like-cooking.info/2013/04/the-london-school-of-test-driven-development/

Slide 24

Slide 24 text

func testCalculatorCallsNthMomentForSecondMomentWithPoint() { let calculatorMock = OCPartialMock(calculator) OCMExpect(calculator.nthMomentAboutPoint(1.0, n: 2.0)) calculator.secondMomentAboutPoint(1.0) XCTAssetNoThrow(calculator.verify()) } ❌

Slide 25

Slide 25 text

OCMOCK, MOCKITO

Slide 26

Slide 26 text

WORKING WITH LEGACY CODE FAKING COLLABORATORS Sale -(void)scan Hardware Display Show Price and Name

Slide 27

Slide 27 text

Cuckoo

Slide 28

Slide 28 text

REFACTORING WITH TESTS

Slide 29

Slide 29 text

KENT BECK, XP EXPLAINED FOUR RULES OF SIMPLE DESIGN • Runs all the tests • Contains no duplication • Expresses the intent of the programmer • Minimises the user of classes or methods (The Rules are in the order of importance) REFACTORING

Slide 30

Slide 30 text

” “ — Uncle Bob, Clean Code THE FACT THAT WE HAVE ALL THE TESTS ELIMINATES THE FEAR CLEANING UP THE CODE WILL BREAK IT

Slide 31

Slide 31 text

class InMemoryDirectory { private var elements = [Element]() func addElement(element: Element) { elements.append(element) } var elementCount: Int { get { return elements.count } } func element(name: String) -> Element? { guard let index = ( elements.indexOf { (element) -> Bool in return element.name == name } ) else { return nil } return elements[index] } func generateIndex() { var index = Element(name: "Index") for element in elements { index.addText(element.name + "\n") } addElement(index) } } ISSUE “Change InMemoryDirectory so that clients can add elements and that the index be maintained.” Guard existing behaviour, before adding new ones.

Slide 32

Slide 32 text

MICHAEL FEATHERS, WORKING EFFECTIVELY WITH LEGACY CODE THE LEGACY CODE CHANGE ALGORITHM • Identify Change Points • Find Test Points • Break Dependancies • Write Tests • Make Changes and Refactor class InMemoryDirectory { private var elements = [Element]() func addElement(element: Element) { elements.append(element) } var elementCount: Int { get { return elements.count } } func element(name: String) -> Element? { guard let index = ( elements.indexOf { (element) -> Bool in return element.name == name } ) else { return nil } return elements[index] } func generateIndex() { var index = Element(name: "Index") for element in elements { index.addText(element.name + "\n") } addElement(index) } }

Slide 33

Slide 33 text

class InMemoryDirectory { private var elements = [Element]() func addElement(element: Element) { elements.append(element) } var elementCount: Int { get { return elements.count } } func element(name: String) -> Element? { guard let index = ( elements.indexOf { (element) -> Bool in return element.name == name } ) else { return nil } return elements[index] } func generateIndex() { var index = Element(name: "Index") for element in elements { index.addText(element.name + "\n") } addElement(index) } } generateIndex elements addElement getElement getElementCount newElement addText newElement.name creates struct Element { var name: String mutating func addText(text: String) { name = name + text } }

Slide 34

Slide 34 text

CHANGING LEGACY CODE CODE EXCERCISE Our tests aren’t so fast. Lets speed them up! I’ll show you a trick.

Slide 35

Slide 35 text

CODE COVERAGE EXCERCISE - Enable Code Coverage

Slide 36

Slide 36 text

XCODE COVERAGE SLATHER https://github.com/SlatherOrg/slather

Slide 37

Slide 37 text

• gem install slather • slather coverage -s --scheme ChangingLegacyCode • slather coverage --html --show --scheme ChangingLegacyCode

Slide 38

Slide 38 text

BREAKING DEPENDANCIES REFACTORING WITH TESTS

Slide 39

Slide 39 text

WORKING EFFECTIVELY WITH LEGACY CODE WHY DO WE BREAK DEPENDENCIES • SENSING
 
 “WE BREAK DEPENDANCIES TO SENSE WHEN WE CAN’T ACCESS VALUES OUR CODE COMPUTES” • SEPARATION
 
 “WE BREAK DEPENDANCIES TO SEPARATE WHEN WE CAN’T EVEN GET A PIECE OF CODE INTO A TEST HARNESS TO RUN”

Slide 40

Slide 40 text

MUSIC DIFF #import #import "MusicDiff.h" #import "MusicDatabase.h" #import "MusicTrack.h" #import "MusicTrackListen.h" #import #import "NetworkManager.h" #import "DatabaseConnection.h" #import "DatabaseTransaction.h" @interface MusicDiff () @property (nonatomic,strong)__block NSMutableArray *tracksToProcess; @property (nonatomic) __block BOOL processing; @end NSString * const kMusicUpdating = @"MusicDiffProcessingNotification"; NSString * const kMusicPercentComplete = @"MusicDiffProcessingPercentComplete"; @implementation MusicDiff + (void)initialize { if (self == [MusicDiff class]) { } } + (MusicDiff *)sharedManager { static MusicDiff *_sharedManager; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ _sharedManager = [[MusicDiff alloc] init]; }); return _sharedManager; } - (void) process { if(_processing) return; [self setProcessing:YES]; if (_tracksToProcess==nil) { _tracksToProcess = [[NSMutableArray alloc]init]; }else{ [_tracksToProcess removeAllObjects]; } [[MusicDatabase sharedManager] allTracksWithCompletionHandler:^(NSArray *songs) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0),^{ __block NSInteger lastReadDate = 0; if([[NSUserDefaults standardUserDefaults]integerForKey:@"lastItunesProcessDate"]>0){ lastReadDate = [[NSUserDefaults standardUserDefaults]integerForKey:@"lastItunesProcessDate"]; }else{ lastReadDate = [[NSDate dateWithTimeIntervalSinceNow:-2628000]timeIntervalSince1970]; } MPMediaQuery *query = [[MPMediaQuery alloc] init]; [query addFilterPredicate:[MPMediaPropertyPredicate predicateWithValue:[NSNumber numberWithInteger:MPMediaTypeMusic] forProperty:MPMediaItemPropertyMediaType]]; NSArray *items = [query items]; NSSortDescriptor *sorter = [NSSortDescriptor sortDescriptorWithKey:MPMediaItemPropertyPlayCount ascending:NO]; NSArray *newItems = [items sortedArrayUsingDescriptors:@[sorter]]; NSMutableArray *playedItems = [NSMutableArray array]; for (MPMediaItem *item in newItems) { if([item playCount]>0){ [playedItems addObject:item]; } } newItems=nil; query=nil; __block NSMutableArray *itemsToUpdate = [NSMutableArray array]; if([playedItems count]){ for (int i =0; i<[playedItems count]; i++) { NSString *trackID = [NSString stringWithFormat:@"%llu", [(NSNumber *)[(MPMediaItem*)[playedItems objectAtIndex:i] valueForProperty:MPMediaItemPropertyPersistentID] unsignedLongLongValue]]; BOOL shouldUpdate = NO; BOOL hasBeenFound = NO; for (MusicTrack *track in songs) { if ([track.trackID isEqualToString:trackID]) { hasBeenFound = YES; if([[(MPMediaItem*)[playedItems objectAtIndex:i]valueForProperty:MPMediaItemPropertyPlayCount]intValue]>[[track numberOfPlays]intValue]){ shouldUpdate=YES; } } } if(shouldUpdate||!hasBeenFound){ [itemsToUpdate addObject:(MPMediaItem*)[playedItems objectAtIndex:i]]; } } if([itemsToUpdate count]){ __block int index = 0; __block NSMutableArray *listens = [NSMutableArray array]; for (MPMediaItem *item in itemsToUpdate) { NSString *trackID = [NSString stringWithFormat:@"%llu", [(NSNumber *)[item valueForProperty:MPMediaItemPropertyPersistentID] unsignedLongLongValue]]; [[MusicDatabase sharedManager] insertOrUpdateTrackWithMediaItemID:trackID andMPMediaItem:item withCompletionHandler:^(MusicTrack *track) { if(track){ [MusicTrackListen createListenWithTrack:track andCompletion:^(MusicTrackListen *listen) { [listens addObject:listen]; index++; [[NSNotificationCenter defaultCenter]postNotificationName:kMusicUpdating object:nil userInfo:@{kMusicPercentComplete:[NSNumber numberWithFloat:(float) ((float)index/(float)[itemsToUpdate count])]}]; if(index==[itemsToUpdate count]){ if([listens count]){ NSMutableDictionary *passingDictionary = [[NSMutableDictionary alloc]initWithDictionary: @{@"deviceId":[[[ASIdentifierManager sharedManager] advertisingIdentifier] UUIDString],@"timestamp":@((long long)([[NSDate date] timeIntervalSince1970]*1000))}]; NSMutableArray *listensArray = [NSMutableArray array]; for (MusicTrackListen *listen in listens) { [listensArray addObject:listen.jsonRepresentation]; } if([[NSUserDefaults standardUserDefaults]boolForKey:@"passedInitialListens"]){ [passingDictionary setObject:[NSNumber numberWithBool:NO] forKey:@"historical"]; }else{ [passingDictionary setObject:[NSNumber numberWithBool:YES] forKey:@"historical"]; [[NSUserDefaults standardUserDefaults]setBool:YES forKey:@"passedInitialListens"]; [[NSUserDefaults standardUserDefaults]synchronize]; Hidden Dependancies Singleton Accessors Asynchronous GCD Code Long Methods System Dependancies Zero Test Coverage

Slide 41

Slide 41 text

” “ — Michael Feathers, Working Effectively with Legacy Code A SEAM IS A PLACE WHERE YOU CAN ALTER BEHAVIOUR IN YOUR PROGRAM WITHOUT EDITING IN THAT PLACE

Slide 42

Slide 42 text

SEAM EXAMPLES

Slide 43

Slide 43 text

static SharedGlobalResource *_sharedInstance; + (id)sharedInstance { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ _sharedTracker = [[SharedGlobalResource _hiddenAlloc] init]; }); } BREAKING DEPENDANCIES 1. INTRODUCE STATIC SETTER static SharedGlobalResource *_sharedInstance; + (id)sharedInstance { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ _sharedTracker = [[SharedGlobalResource alloc] init]; }); } + (void)setTestingSharedInstance:(SharedGlobalResource *)sharedInstance { [SharedGlobalResource sharedTracker]; _sharedInstance = sharedTracker; } - (void)testExample { SharedGlobalResource *tracker = [[TestableSharedGlobalResourceSpy alloc] init]; [SharedGlobalResource setTestingSharedTracker:tracker]; } SEAM

Slide 44

Slide 44 text

BREAKING DEPENDANCIES 2. EXTRACT AND OVERRIDE CALL + (id)alloc{ [NSException raise:@"Creating SharedGlobalResource is not allowed. Use sharedInstance instead" format:@“"]; return nil; } + (id)new { return [SharedGlobalResource alloc]; } + (id)_hiddenAlloc /* Private */ { return [super alloc]; } // Subclass and Override in Test Target @interface SharedGlobalResource () + (id)_hiddenAlloc; @end @implementation TestableSharedGlobalResource: SharedGlobalResource + (id)alloc { return [super _hiddenAlloc]; } @end

Slide 45

Slide 45 text

- (void) execute:(dispatch_block_t)completion { [self dispatchAsync:^{ // ... completion(); }]; } - (void)dispatchAsync:(dispatch_block_t)block { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0),block); } BREAKING DEPENDANCIES 2. EXTRACT AND OVERRIDE CALL - (void) execute:(dispatch_block_t)completion { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{ // ... completion(); }); } @interface TestableAsyncCommand: AsyncCommand // Subclass and Override - (void)dispatchAsync:(dispatch_block_t)block { block(); } SEAM

Slide 46

Slide 46 text

BREAKING HIDDEN DEPENDANCIES 3. PARAMETERIZE CONSTRUCTOR, METHOD class MailChecker { let checkPeriodSeconds: Int let mailReceiver = MailReceiver() init(checkPeriodSeconds: Int) { self.checkPeriodSeconds = checkPeriodSeconds } } class MailChecker { let checkPeriodSeconds: Int let mailReceiver: MailReceiver init(checkPeriodSeconds: Int, mailReceiver: MailReceiver = MailReceiver()) { self.checkPeriodSeconds = checkPeriodSeconds self.mailReceiver = mailReceiver } } SEAM