CocoaHeads SKG #1 - Testing iOS Applications

CocoaHeads SKG #1 - Testing iOS Applications

@attheodo speaks about testing iOS Applications and urges us to break our testing taboos

5a3cfb263fe4968b76c4c0bde721f047?s=128

CocoaHeadsSKG

January 27, 2016
Tweet

Transcript

  1. Testing iOS Applications Testing iOS Applications @attheodo @attheodo (Breaking the

    taboos) (Breaking the taboos)
  2. Agenda Agenda What is testing What is testing Configuration Configuration

    Testing Frameworks Testing Frameworks Nimble fundamendals Nimble fundamendals XCTest fundamendals XCTest fundamendals Testings ViewControllers Testings ViewControllers Pitfalls/Bad practices Pitfalls/Bad practices Going further... Going further...
  3. What is testing What is testing Tests are code... Tests

    are code... that run your app's code... that run your app's code... and make sure it behaves and make sure it behaves properly. properly. Properly? Properly? Consistent output for certain input. Consistent output for certain input. Proper behaviour for weird input. Proper behaviour for weird input.
  4. Types of Tests Types of Tests Unit Tests Unit Tests

    Integration Tests Integration Tests User Interface Tests User Interface Tests Performance Tests Performance Tests Cheap Cheap Expensive Expensive
  5. Why test? Why test? Forced to write better code. Forced

    to write better code. Less crapware shipped to customers. Less crapware shipped to customers. Diminished technical debt. Diminished technical debt. Implement new features with Implement new features with confidence. confidence. Detect Regressions (things that Detect Regressions (things that used to work but broke) used to work but broke)
  6. Tests should be FIRST Tests should be FIRST F Fast

    ast I Isolated solated R Repeatable epeatable S Self-verifying elf-verifying T Timely imely
  7. Configuration Configuration

  8. Configuration Configuration

  9. Configuration (current project) Configuration (current project)

  10. Configuration (current project) Configuration (current project)

  11. Distinguishing testing env Distinguishing testing env It's useful to know

    whether your code runs in test It's useful to know whether your code runs in test mode or in debug/release/testflight mode. mode or in debug/release/testflight mode. Compile time (preprocessor macros) Compile time (preprocessor macros) Run-time Run-time Launch-time arguments Launch-time arguments Process info environment Process info environment ObjC Runtime ObjC Runtime
  12. Distinguishing testing env Distinguishing testing env Compile time Compile time

  13. Distinguishing testing env Distinguishing testing env Compile time Compile time

  14. Distinguishing testing env Distinguishing testing env // MARK: Application Lifecycle

    func application( application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { #if !TESTING registerUINotificationStyles() exportApplicationMetadataToNSUserDefaults() loadStoryboard() IRConnectivityManager.sharedInstance.setup() #endif return true } Compile time Compile time
  15. Distinguishing testing env Distinguishing testing env Runtime (Launch Args) Runtime

    (Launch Args)
  16. Distinguishing testing env Distinguishing testing env func isTesting() -> Bool

    { // get launch arguments from NSProcessInfo return NSProcessInfo.processInfo().arguments.contains("isTesting") } Declare it in your AppDelegate/Helpers Declare it in your AppDelegate/Helpers Use it here and there... Use it here and there... Runtime (Launch Args) Runtime (Launch Args)
  17. Distinguishing testing env Distinguishing testing env func isTesting() -> Bool

    { // get launch arguments from NSProcessInfo return NSProcessInfo.processInfo().environment["XCInjectBundle"] != nil } Essentially checks whether Testing Essentially checks whether Testing Bundle has been injected... Bundle has been injected... Declare it in your AppDelegate/Helpers Declare it in your AppDelegate/Helpers Use it here and there... Use it here and there... Runtime (Process Info Environment) Runtime (Process Info Environment)
  18. Distinguishing testing env Distinguishing testing env func isTesting() -> Bool

    { return NSClassFromString("XCTestCase") != nil } Checks with the runtime if `XCTestCase` Checks with the runtime if `XCTestCase` class has been linked class has been linked Declare it in your AppDelegate/Helpers Declare it in your AppDelegate/Helpers Use it here and there... Use it here and there... Runtime (ObjC Runtime) Runtime (ObjC Runtime)
  19. Testing Frameworks Testing Frameworks XCUnit XCUnit Specta Specta BDD/TDD BDD/TDD

    Kiwi Kiwi Cedar Cedar Quick (Swift) Quick (Swift) XCUnit XCUnit Expecta Expecta MATCHERS MATCHERS Nimble (Swift) Nimble (Swift)
  20. Choosing a Testing Framework Choosing a Testing Framework Good Xcode

    integration Good Xcode integration Spec-ing close to your needs/style Spec-ing close to your needs/style BDD/TDD BDD/TDD Mocking/Stubbing support Mocking/Stubbing support MATCHERS MATCHERS Convenient DSL Convenient DSL Async Expectations Async Expectations
  21. My Swifty suggestion as per Jan 2016 My Swifty suggestion

    as per Jan 2016 XCUnit XCUnit BDD/TDD BDD/TDD MATCHER MATCHER Nimble Nimble Best Xcode integration Best Xcode integration Harness is very flexible (barebones) Harness is very flexible (barebones) Looks fast so far (122 tests in 5secs) Looks fast so far (122 tests in 5secs) Nimble has great built-in functions Nimble has great built-in functions and good DSL and good DSL
  22. Nimble Fundamendals Nimble Fundamendals Actual Actual Operator Operator Expected Expected

    expect(...) expect(...) .to(...) .to(...) .notTo(...) .notTo(...) .toNot(...) .toNot(...) equal() equal() beIdenticalTo() beIdenticalTo() beLessThan() beLessThan() beGreaterThan() beGreaterThan() beAnInstanceOf() beAnInstanceOf() beAKindOf() beAKindOf() beTrue() beTrue() beFalse() beFalse() contain() contain() beEmpty() beEmpty()
  23. Nimble Fundamendals Nimble Fundamendals expect(actual).to(equal(expected)) expect(actual).toNot(equal(expected)) expect(actual).to(beLessThan(expected)) expect(button).to(beAnInstanceOf(UIButton)) expect(string).to(beAKindOf(String)) expect(flag).to(beTrue())

    expect(myArray).to(contain('koko')) expect(myURL).to(contain("http://")) expect(myURL).to(endWith(".io")) expect(myArray).to(haveCount(10))
  24. Nimble Fundamendals Nimble Fundamendals expect(actual) == expected expect(actual) < expected

    expect(flag) == true expect(myArray.contains('koko')) == true expect(myArray.count) == 10
  25. Nimble Fundamendals Nimble Fundamendals C Primitives C Primitives Lazily computed

    values Lazily computed values Async Expectations Async Expectations Error Handling (assert for exception throwing) Error Handling (assert for exception throwing) Custom Matchers Custom Matchers Custom Failure Messages Custom Failure Messages RTFM RTFM https://github.com/Quick/Nimble
  26. XCTest Fundamendals XCTest Fundamendals @testable import MyAwesomeApp class MyCalcTests: XCTestCase

    { override func setUp() { super.setUp() } override func tearDown() { super.tearDown() } func testItCorrectlyAddsTwoNumbers() { } func generateNumbers() -> (a: Int, b: Int) { return (a: randomNum(), b: randomNum()) } } Expose your app's public/internal APIs Runs before each function that starts with "test" Runs after each function that starts with "test" A test case! A helper method in this test suite A test suite
  27. XCTest Fundamendals XCTest Fundamendals import Nimble @testable import MyAwesomeApp class

    MyCalcTests: XCTestCase { var calc: MyCalc! override func setUp() { super.setUp() calc = MyCalc() // any additional init setup before each test cases is run } override func tearDown() { super.tearDown() calc.clearCache() // any additional teardown before next case is run } func testItCorrectlyAddsTwoNumbers() { let numbers = generateNumbers() expect(calc.add(numbers.a, numbers.b)) == number.a + numbers.b } func generateNumbers() -> (a: Int, b: Int) { return (a: randomNum(), b: randomNum()) } }
  28. XCTest Fundamendals XCTest Fundamendals ⌘ ⌘ U U Shortcuts you

    must learn. Speed they give you. Shortcuts you must learn. Speed they give you. ctrl ctrl ⌥ ⌥ ⌘ ⌘ U U ⌘ ⌘ 5 5 Run all test suites and all their test cases Run all the cases in current test suite Show the test navigator
  29. XCTest Fundamendals XCTest Fundamendals Steps towards Continuous Deployment Steps towards

    Continuous Deployment xctool xctool $ brew install xctool $ xctool.sh -workspace YourWorkspace.xcworkspace -scheme YourScheme test Scan (Part of fastlane) Scan (Part of fastlane) $ sudo gem install scan $ scan --workspace "YourWorkspace.xcworkspace" --scheme "YourScheme" --device "iPhone 6"
  30. XCTest Fundamendals XCTest Fundamendals Now with testing coverage! Now with

    testing coverage!
  31. XCTest Fundamendals XCTest Fundamendals

  32. XCTest Fundamendals XCTest Fundamendals

  33. Testing ViewControllers Testing ViewControllers Closest to what your end user

    sees. Closest to what your end user sees. Test the crap out of them. Test the crap out of them. But remember, keep them thin. But remember, keep them thin. Xcode UI Tests are cool. Xcode UI Tests are cool. But quite slow... But quite slow... Treat them as integration-ish tests. Treat them as integration-ish tests. Unit-test their dependencies. Unit-test their dependencies.
  34. Testing ViewControllers Testing ViewControllers What we need? What we need?

    A setup to trigger viewDidLoad() A setup to trigger viewDidLoad() Reference IBOutlets and trigger events. Reference IBOutlets and trigger events. A way to detect view controller A way to detect view controller state/hierarchy (UIAlertControllers etc) state/hierarchy (UIAlertControllers etc) Ocassionally, a UI stack for Ocassionally, a UI stack for pushing/popping vc's. pushing/popping vc's.
  35. Testing ViewControllers Testing ViewControllers A setup to trigger viewDidLoad() A

    setup to trigger viewDidLoad() func testViewControllerSetsValueOnViewDidLoad() { let vc = MyViewController() let _ = vc.view // This triggers viewDidLoad() expect(vc.someValueSetOnViewDidLoad) == true } Accessing view property triggers Accessing view property triggers viewDidLoad() viewDidLoad() No luck for viewDid/WillAppear() and the No luck for viewDid/WillAppear() and the rest though... rest though...
  36. Testing ViewControllers Testing ViewControllers A UI stack for poping/pushing VCs

    A UI stack for poping/pushing VCs func testViewControllerSetsValueOnViewDidLoad() { let s = UIStoryboard(name: "Main", bundle: nil) vc = s.instantiateViewControllerWithIdentifier("VcStoryboardId") as! MyViewController let dl = UIApplication.sharedApplication().delegate as! AppDelegate dl.window = UIWindow(frame: UIScreen.mainScreen().bounds) dl.window!.rootViewController = UINavigationController(rootViewController: vc) dl.window!.makeKeyAndVisible() let _ = vc.view // This triggers viewDidLoad() } Now we have a UIWindow that can interpret touch Now we have a UIWindow that can interpret touch events etc. events etc. We can push pop view controllers using storyboard We can push pop view controllers using storyboard
  37. Testing ViewControllers Testing ViewControllers func testPressingLogoutButtonLogoutsTheUser() { let vc =

    MyLoginViewController() let _ = vc.view expect(vc.logoutButton).to(beAnInstanceOf(UIButton)) // "tap" the logout button vc.logoutButton.sendActionsForControlEvents(.TouchedUpInside) expect(MyAuthManager.sharedInstance.isLoggedIn) == false } Reference IBOutlets and trigger events. Reference IBOutlets and trigger events. IBOutlets are "public" properties of the VC IBOutlets are "public" properties of the VC Sending tap actions actually tests they are wired with a UIAction (do Sending tap actions actually tests they are wired with a UIAction (do not just call their selector) not just call their selector)
  38. Testing ViewControllers Testing ViewControllers func testPressingLogoutButtonPresentsUIAlertWarning() { let s =

    UIStoryboard(name: "Main", bundle: nil) vc = s.instantiateViewControllerWithIdentifier("VcStoryboardId") as! MyViewController let dl = UIApplication.sharedApplication().delegate as! AppDelegate dl.window = UIWindow(frame: UIScreen.mainScreen().bounds) dl.window!.rootViewController = UINavigationController(rootViewController: vc) dl.window!.makeKeyAndVisible() let _ = vc.view // This triggers viewDidLoad() expect(vc.logoutButton).to(beAnInstanceOf(UIButton)) // "tap" the logout button vc.logoutButton.sendActionsForControlEvents(.TouchedUpInside) // check that view controller presented a UIAlertController expect(vc.presentedViewController).to(beAnInstanceOf(UIAlertController)) } Detect viewcontroller's state/hierarchy Detect viewcontroller's state/hierarchy
  39. Bad Practices Bad Practices Specifying small individual units integrating the

    whole system Unit Tests Integration Tests Dirty Hybrids Good Good Bad! (Unclear goal, high maintainance) Always search for the "sweet spot" Always search for the "sweet spot"
  40. Bad Practices Bad Practices func testColor() { // given let

    dog = Dog(breed: "German Shepherd") // then expect(dog.furColor) == UIColor.brownColor() } func testGermanShepherdDogBreedHasProperColor() { // given let dog = Dog(breed: "German Shepherd") // then expect(dog.furColor) == UIColor.brownColor() } Good Bad! Shitty names Shitty names
  41. Bad Practices Bad Practices Testing private methods Testing private methods

    Private means Private means Private Private dude. dude. If you feel the urge to test private methods, there's If you feel the urge to test private methods, there's something wrong with that method. something wrong with that method. It's probably doing too much. It's probably doing too much. What to do? What to do? Uhmm.. refactor? Uhmm.. refactor? Test the private methods through the public Test the private methods through the public API API Extract method into a static class with a contract Extract method into a static class with a contract
  42. Bad Practices Bad Practices Coupling tests with implementation details Coupling

    tests with implementation details NOWHERE in your tests you should call APIs of NOWHERE in your tests you should call APIs of external libraries. external libraries. Mocking/stubbing is ok. But make sure you test the Mocking/stubbing is ok. But make sure you test the mocked/stubbed logic independently. mocked/stubbed logic independently. Testing constructors Testing constructors Constructors don't and shouldn't have Constructors don't and shouldn't have behaviour behaviour They are "implementation details" They are "implementation details"
  43. Going further.... Going further.... Stubbing/Mocking objects Stubbing/Mocking objects Stubbing Network

    requests Stubbing Network requests Continuous Deployment Continuous Deployment
  44. Questions? Questions? @attheodo @attheodo http:/ /attheo.do http:/ /attheo.do