Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Testing ⌚️ Apps and Other Extensions

Testing ⌚️ Apps and Other Extensions

Talk on testing watch apps and other iOS application extensions.

Boris Bügling

July 08, 2016
Tweet

More Decks by Boris Bügling

Other Decks in Programming

Transcript

  1. > 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"); }); });
  2. 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
  3. TEST it(@"should show the correct text", ^{ [subject willActivate]; subject.label

    should have_received(@selector(setText:)).with(@"Yay WatchKit!"); });
  4. > 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
  5. MODEL struct Person { let salutation: String let firstName: String

    let lastName: String let birthdate: Date }
  6. 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)) } }
  7. 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) }
  8. UPDATED WKINTERFACECONTROLLER override func awakeWithContext(context: AnyObject!) { if let viewModel

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

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

    Gesture recognizers and crown interactions > Tailored background tasks and local notifications
  12. > 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];
  13. Some of them are more like integrations, though: > Today

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

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

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

    macros for conditional compilation > CocoaPods subspecs can help here
  17. 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
  18. 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