Save 37% off PRO during our Black Friday Sale! »

Testing ⌚️ Apps and Other Extensions

Testing ⌚️ Apps and Other Extensions

Talk on testing watch apps and other iOS application extensions.

9d2ea021919ff81e02d48530aae191bd?s=128

Boris Bügling

July 08, 2016
Tweet

Transcript

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

  2. COCOAPODS

  3. CONTENTFUL

  4. None
  5. ⌚ APPS

  6. NO TEST TARGETS !

  7. None
  8. NO CHANGES IN WATCHOS 3

  9. POSSIBILITIES

  10. YOLO, JUST DON'T WRITE BUGS

  11. None
  12. USE PivotalCoreKit

  13. > 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"); }); });
  14. 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
  15. IMPLEMENTATION @implementation MyInterfaceController - (void)willActivate { [super willActivate]; [self.label setText:@"Yay

    WatchKit!"]; } @end
  16. TEST it(@"should show the correct text", ^{ [subject willActivate]; subject.label

    should have_received(@selector(setText:)).with(@"Yay WatchKit!"); });
  17. > 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
  18. MOVE CODE INTO A FRAMEWORK

  19. > Model => Framework > Presentation Logic => Framework >

    View => Watch Extension
  20. MVVM

  21. VIEW MODEL CONTAINS THE PRESENTATION LOGIC

  22. MODEL struct Person { let salutation: String let firstName: String

    let lastName: String let birthdate: Date }
  23. 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)) } }
  24. VIEWMODEL struct PersonViewModel { let nameText: String let birthdateText: String

    }
  25. 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) }
  26. UPDATED WKINTERFACECONTROLLER override func awakeWithContext(context: AnyObject!) { if let viewModel

    = context as? PersonViewModel { nameLabel.setText(viewModel.nameText) birthdateLabel.setText(viewModel.birthdateText) } }
  27. 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
  28. UI TESTS? > Not supported on watchOS, either > There

    is almost no view code on watchOS <= 2 > WatchKit is essentially a ViewModel itself
  29. WATCHOS 3 > Custom UI with SpriteKit / SceneKit >

    Gesture recognizers and crown interactions > Tailored background tasks and local notifications
  30. RDAR://21760513

  31. APPLICATION EXTENSIONS

  32. SIMILAR TO WATCH APPS, NO TEST BUNDLES SPECIFIC TO EXTENSIONS

  33. > 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];
  34. Some of them are more like integrations, though: > Today

    extensions > iMessage extensions > Sticker packs > Intents (SiriKit) > ...
  35. SOLUTION > Also move code into frameworks > Use MVVM

    to make code testable outside of the extension context
  36. SHARING A FRAMEWORK BETWEEN EXTENSIONS AND APPS > Set APPLICATION_EXTENSION_API_ONLY

    for compile time safety > Use NS_EXTENSION_UNAVAILABLE_IOS for disallowing symbols
  37. SHARING A FRAMEWORK BETWEEN EXTENSIONS AND APPS > Use custom

    macros for conditional compilation > CocoaPods subspecs can help here
  38. 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
  39. PODFILE target 'App' do pod 'GTMSessionFetcher' end target 'Extension' do

    pod 'GTMSessionFetcher/AppExtension' end
  40. 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
  41. THANK YOU!

  42. @NeoNacho boris@contentful.com http://buegling.com/talks