Slide 1

Slide 1 text

TESTING ⌚ APPS AND OTHER EXTENSIONS BORIS BÜGLING - @NEONACHO

Slide 2

Slide 2 text

COCOAPODS

Slide 3

Slide 3 text

CONTENTFUL

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

⌚ APPS

Slide 6

Slide 6 text

NO TEST TARGETS !

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

NO CHANGES IN WATCHOS 3

Slide 9

Slide 9 text

POSSIBILITIES

Slide 10

Slide 10 text

YOLO, JUST DON'T WRITE BUGS

Slide 11

Slide 11 text

No content

Slide 12

Slide 12 text

USE PivotalCoreKit

Slide 13

Slide 13 text

> Pivotal's collection of helpers for iOS > Includes a bunch of testing infrastructure > Works together with their BDD framework Cedar: describe(@"Example specs on NSString", ^{ it(@"lowercaseString returns a new string with everything in lower case", ^{ [@"FOOBar" lowercaseString] should equal(@"foobar"); }); });

Slide 14

Slide 14 text

TESTING SETUP #import "Cedar.h" #import "MyInterfaceController.h" #import "PCKInterfaceControllerLoader.h" using namespace Cedar::Matchers; using namespace Cedar::Doubles; SPEC_BEGIN(MyInterfaceControllerSpec) describe(@"MyInterfaceController", ^{ __block PCKInterfaceControllerLoader *loader; __block MyInterfaceController *subject; beforeEach(^{ NSBundle *testBundle = [NSBundle bundleForClass:[self class]]; loader = [[PCKInterfaceControllerLoader alloc] init]; subject = [loader interfaceControllerWithStoryboardName:@"Interface" identifier:@"myId" bundle:testBundle]; }); // ... }); SPEC_END

Slide 15

Slide 15 text

IMPLEMENTATION @implementation MyInterfaceController - (void)willActivate { [super willActivate]; [self.label setText:@"Yay WatchKit!"]; } @end

Slide 16

Slide 16 text

TEST it(@"should show the correct text", ^{ [subject willActivate]; subject.label should have_received(@selector(setText:)).with(@"Yay WatchKit!"); });

Slide 17

Slide 17 text

> Test doubles for all of WatchKit > Huge 3rd party dependency for your testsuite > Has to catch up with new versions of watchOS > Tests run on macOS only > Doesn't work very well with Swift

Slide 18

Slide 18 text

MOVE CODE INTO A FRAMEWORK

Slide 19

Slide 19 text

> Model => Framework > Presentation Logic => Framework > View => Watch Extension

Slide 20

Slide 20 text

MVVM

Slide 21

Slide 21 text

VIEW MODEL CONTAINS THE PRESENTATION LOGIC

Slide 22

Slide 22 text

MODEL struct Person { let salutation: String let firstName: String let lastName: String let birthdate: Date }

Slide 23

Slide 23 text

WKINTERFACECONTROLLER override func awakeWithContext(context: AnyObject!) { if let model = context as? Person { if model.salutation.characters.count > 0 { nameLabel.setText("\(model.salutation) \(model.firstName) \(model.lastName)") } else { nameLabel.setText("\(model.firstName) \(model.lastName)") } let dateFormatter = DateFormatter() dateFormatter.dateFormat = "EEEE MMMM d, yyyy" birthdateLabel.setText(dateFormatter.string(from: model.birthdate)) } }

Slide 24

Slide 24 text

VIEWMODEL struct PersonViewModel { let nameText: String let birthdateText: String }

Slide 25

Slide 25 text

VIEWMODEL UPDATE LOGIC if let model = model as? Person { if model.salutation.characters.count > 0 { nameText = "\(model.salutation) \(model.firstName) \(model.lastName)" } else { nameText = "\(model.firstName) \(model.lastName)" } let dateFormatter = DateFormatter() dateFormatter.dateFormat = "EEEE MMMM d, yyyy" birthdateText = dateFormatter.string(from: model.birthdate) }

Slide 26

Slide 26 text

UPDATED WKINTERFACECONTROLLER override func awakeWithContext(context: AnyObject!) { if let viewModel = context as? PersonViewModel { nameLabel.setText(viewModel.nameText) birthdateLabel.setText(viewModel.birthdateText) } }

Slide 27

Slide 27 text

WHAT DID WE GAIN? > Our presentation logic does not depend on WatchKit anymore > It can be moved to a cross-platform framework > It can then be tested on macOS or iOS

Slide 28

Slide 28 text

UI TESTS? > Not supported on watchOS, either > There is almost no view code on watchOS <= 2 > WatchKit is essentially a ViewModel itself

Slide 29

Slide 29 text

WATCHOS 3 > Custom UI with SpriteKit / SceneKit > Gesture recognizers and crown interactions > Tailored background tasks and local notifications

Slide 30

Slide 30 text

RDAR://21760513

Slide 31

Slide 31 text

APPLICATION EXTENSIONS

Slide 32

Slide 32 text

SIMILAR TO WATCH APPS, NO TEST BUNDLES SPECIFIC TO EXTENSIONS

Slide 33

Slide 33 text

> Extension can be launched from main app in UI tests > Since it is a remote view controller, only coordinates work: XCUICoordinate* coordinateOfRowThatLaunchesYourExtension = [app coordinateWithNormalizedOffset:CGVectorMake(0.5, 603.0 / 736.0)]; [coordinateOfRowThatLaunchesYourExtension tap];

Slide 34

Slide 34 text

Some of them are more like integrations, though: > Today extensions > iMessage extensions > Sticker packs > Intents (SiriKit) > ...

Slide 35

Slide 35 text

SOLUTION > Also move code into frameworks > Use MVVM to make code testable outside of the extension context

Slide 36

Slide 36 text

SHARING A FRAMEWORK BETWEEN EXTENSIONS AND APPS > Set APPLICATION_EXTENSION_API_ONLY for compile time safety > Use NS_EXTENSION_UNAVAILABLE_IOS for disallowing symbols

Slide 37

Slide 37 text

SHARING A FRAMEWORK BETWEEN EXTENSIONS AND APPS > Use custom macros for conditional compilation > CocoaPods subspecs can help here

Slide 38

Slide 38 text

EXAMPLE: GTMSESSIONFETCHER Disables GTM_BACKGROUND_TASK_FETCHING in application extension subspec: s.subspec 'AppExtension' do |ap| ap.source_files = … ap.pod_target_xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) GTM_BACKGROUND_TASK_FETCHING=0' } end

Slide 39

Slide 39 text

PODFILE target 'App' do pod 'GTMSessionFetcher' end target 'Extension' do pod 'GTMSessionFetcher/AppExtension' end

Slide 40

Slide 40 text

CONCLUSION > Move almost all code to frameworks > Those can be tested like any other part of your codebase > Use MVVM to have as little logic as possible in the actual extension environment

Slide 41

Slide 41 text

THANK YOU!

Slide 42

Slide 42 text

@NeoNacho [email protected] http://buegling.com/talks