Testing iOS Applications

@attheodo

(Breaking the taboos)

Agenda

  • What is testing

  • Configuration

  • Testing Frameworks

  • Nimble fundamendals

  • XCTest fundamendals

  • Testings ViewControllers

  • Pitfalls/Bad practices

  • Going further...

What is testing

  • Tests are code...

  • that run your app's code...

  • and make sure it behaves properly.

Properly?

  • Consistent output for certain input.

  • Proper behaviour for weird input.

Types of Tests

  • Unit Tests

  • Integration Tests

  • User Interface Tests

  • Performance Tests

Cheap

Expensive

Why test?

  • Forced to write better code.

  • Less crapware shipped to customers.

  • Diminished technical debt.

  • Implement new features with confidence.

  • Detect Regressions (things that used to work but broke)

Tests should be FIRST

  • Fast

  • Isolated

  • Repeatable

  • Self-verifying

  • Timely

Configuration

Configuration

Configuration (current project)

Configuration (current project)

Distinguishing testing env

It's useful to know whether your code runs in test mode or in debug/release/testflight mode.

  • Compile time (preprocessor macros)

  • Run-time

  • Launch-time arguments

  • Process info environment

  • ObjC Runtime

Distinguishing testing env 

Compile time

Distinguishing testing env 

Compile time

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

Distinguishing testing env

Runtime (Launch Args)

Distinguishing testing env 

 func isTesting() -> Bool {

     // get launch arguments from NSProcessInfo
     return NSProcessInfo.processInfo().arguments.contains("isTesting")

 }
  • Declare it in your AppDelegate/Helpers

  • Use it here and there...

Runtime (Launch Args)

Distinguishing testing env 

 func isTesting() -> Bool {

     // get launch arguments from NSProcessInfo
     return NSProcessInfo.processInfo().environment["XCInjectBundle"] != nil
     
 }
  • Essentially checks whether Testing Bundle has been injected...

  • Declare it in your AppDelegate/Helpers

  • Use it here and there...

Runtime (Process Info Environment)

Distinguishing testing env 

 func isTesting() -> Bool {

     return NSClassFromString("XCTestCase") != nil
     
 }
  • Checks with the runtime if `XCTestCase` class has been linked

  • Declare it in your AppDelegate/Helpers

  • Use it here and there...

Runtime (ObjC Runtime)

Testing Frameworks

  • XCUnit

  • Specta

BDD/TDD

  • Kiwi

  • Cedar

  • Quick (Swift)

  • XCUnit

  • Expecta

MATCHERS

  • Nimble (Swift)

Choosing a Testing Framework

  • Good Xcode integration

  • Spec-ing close to your needs/style

BDD/TDD

  • Mocking/Stubbing support

MATCHERS

  • Convenient DSL

  • Async Expectations

My Swifty suggestion as per Jan 2016

  • XCUnit

BDD/TDD

MATCHER

  • Nimble

  • Best Xcode integration

  • Harness is very flexible (barebones)

  • Looks fast so far (122 tests in 5secs)

  • Nimble has great built-in functions and good DSL

Nimble Fundamendals

Actual

Operator

Expected

expect(...)

.to(...)

.notTo(...)

.toNot(...)

equal()

beIdenticalTo()

beLessThan()

beGreaterThan()

beAnInstanceOf()

beAKindOf()

beTrue()

beFalse()

contain()

beEmpty()

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

Nimble Fundamendals

expect(actual) == expected

expect(actual) < expected

expect(flag) == true

expect(myArray.contains('koko')) == true

expect(myArray.count) == 10

Nimble Fundamendals

  • C Primitives

  • Lazily computed values

  • Async Expectations

  • Error Handling (assert for exception throwing)

  • Custom Matchers

  • Custom Failure Messages

RTFM

https://github.com/Quick/Nimble

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

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

}

XCTest Fundamendals

  • ⌘ U

Shortcuts you must learn. Speed they give you.

  • ctrl ⌥ ⌘ U

  • ⌘ 5

Run all test suites and all their test cases

Run all the cases in current test suite

Show the test navigator

XCTest Fundamendals

Steps towards Continuous Deployment

  • xctool

$ brew install xctool
$ xctool.sh -workspace YourWorkspace.xcworkspace -scheme YourScheme test
  • Scan (Part of fastlane)

$ sudo gem install scan
$ scan --workspace "YourWorkspace.xcworkspace" --scheme "YourScheme" --device "iPhone 6"

XCTest Fundamendals

Now with testing coverage!

XCTest Fundamendals

XCTest Fundamendals

Testing ViewControllers

  • Closest to what your end user sees.

  • Test the crap out of them.

  • But remember, keep them thin.

  • Xcode UI Tests are cool.

  • But quite slow...

  • Treat them as integration-ish tests.

  • Unit-test their dependencies.

Testing ViewControllers

What we need?

  • A setup to trigger viewDidLoad()

  • Reference IBOutlets and trigger events.

  • A way to detect view controller state/hierarchy (UIAlertControllers etc)

  • Ocassionally, a UI stack for pushing/popping vc's.

Testing ViewControllers

A setup to trigger viewDidLoad()

func testViewControllerSetsValueOnViewDidLoad() {

    let vc = MyViewController()
    let _ = vc.view // This triggers viewDidLoad()

    expect(vc.someValueSetOnViewDidLoad) == true

}
  • Accessing view property triggers viewDidLoad()

  • No luck for viewDid/WillAppear() and the rest though...

Testing ViewControllers

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

  • We can push pop view controllers using storyboard

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.

  • IBOutlets are "public" properties of the VC

  • Sending tap actions actually tests they are wired with a UIAction (do not just call their selector)

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

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"

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

Bad Practices

Testing private methods

  • Private means Private dude.

  • If you feel the urge to test private methods, there's something wrong with that method.

  • It's probably doing too much.

  • What to do?

  • Uhmm.. refactor?

  • Test the private methods through the public API

  • Extract method into a static class with a contract

Bad Practices

Coupling tests with implementation details

  • NOWHERE in your tests you should call APIs of external libraries.

  • Mocking/stubbing is ok. But make sure you test the mocked/stubbed logic independently.

  • Testing constructors

  • Constructors don't and shouldn't have behaviour

  • They are "implementation details"

Going further....

  • Stubbing/Mocking objects

  • Stubbing Network requests

  • Continuous Deployment

Questions?

@attheodo

http://attheo.do

Testing iOS Applications

By Thanos Theodoridis

Testing iOS Applications

  • 633