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

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

CocoaHeadsSKG

January 27, 2016
Tweet

More Decks by CocoaHeadsSKG

Other Decks in Technology

Transcript

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

    View full-size slide

  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...

    View full-size slide

  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.

    View full-size slide

  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

    View full-size slide

  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)

    View full-size slide

  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

    View full-size slide

  7. Configuration
    Configuration

    View full-size slide

  8. Configuration
    Configuration

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  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

    View full-size slide

  12. Distinguishing testing env
    Distinguishing testing env
    Compile time
    Compile time

    View full-size slide

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

    View full-size slide

  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

    View full-size slide

  15. Distinguishing testing env
    Distinguishing testing env
    Runtime (Launch Args)
    Runtime (Launch Args)

    View full-size slide

  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)

    View full-size slide

  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)

    View full-size slide

  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)

    View full-size slide

  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)

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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()

    View full-size slide

  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))

    View full-size slide

  24. Nimble Fundamendals
    Nimble Fundamendals
    expect(actual) == expected
    expect(actual) < expected
    expect(flag) == true
    expect(myArray.contains('koko')) == true
    expect(myArray.count) == 10

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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())
    }
    }

    View full-size slide

  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

    View full-size slide

  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"

    View full-size slide

  30. XCTest Fundamendals
    XCTest Fundamendals
    Now with testing coverage!
    Now with testing coverage!

    View full-size slide

  31. XCTest Fundamendals
    XCTest Fundamendals

    View full-size slide

  32. XCTest Fundamendals
    XCTest Fundamendals

    View full-size slide

  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.

    View full-size slide

  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.

    View full-size slide

  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...

    View full-size slide

  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

    View full-size slide

  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)

    View full-size slide

  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

    View full-size slide

  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"

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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"

    View full-size slide

  43. Going further....
    Going further....
    Stubbing/Mocking objects
    Stubbing/Mocking objects
    Stubbing Network requests
    Stubbing Network requests
    Continuous Deployment
    Continuous Deployment

    View full-size slide

  44. Questions?
    Questions?
    @attheodo
    @attheodo http:/
    /attheo.do
    http:/
    /attheo.do

    View full-size slide