Agile Swift

Godfrey Nolan

RIIS LLC

Agenda

  • Unit Testing intro
  • Up and Running
  • Swift Flavors
  • Unit testing 101
  • Tools of the trade
  • Mocking
  • User Interface testing

Unit Testing Intro

  • Hello World
  • Benefits
  • Testing Pyramid
  • UI Testing

Unit Testing Intro

import XCTest
@testable import HelloWorld

class HelloWorldTests: XCTestCase {

    func testExample() {
        XCTAssertEqual(HelloWorld().text, "Hello, World!")
    }

}
struct HelloWorld {

    var text = "Hello, World!"

}

Unit Testing Benefits

  • Catch more mistakes
  • Confidently make more changes
  • Built in regression testing
  • Extend the life of your codebase

Swift 3.x Flavors

  • MacOS
    • Xcode, command line and repl
  • Linux 
    • Ubuntu 14.04 and 16.04
    • Command line and repl
    • No GUI libraries
  • Swift Playground on iPad
  • More flavors to come?

Interactive Swift

  • Good language learning environment 
  • Options
    • Playgrounds
    • Repl

Command Line Swift

  • Headless Swift
  • First class cousin of Xcode Swift
  • Package Manager
    • Demo swift package init
  • Primary option on Linux
    • Docker
    • AWS Free tier
  • Also works on MacOS

Package Manager

Package.swift

import PackageDescription

let package = Package(
    name: "HelloWorld"
)
import PackageDescription

let package = Package(
    name: "DeckOfPlayingCards",
    dependencies: [
        .Package(url: "https://github.com/apple/example-package-fisheryates.git", majorVersion: 2),
        .Package(url: "https://github.com/apple/example-package-playingcard.git", majorVersion: 3),
    ]
)

LinuxMain.swift

import XCTest
@testable import HelloWorldTests

XCTMain([
     testCase(HelloWorldTests.allTests),
])

HelloWorldTests.swift

import XCTest
@testable import HelloWorld

class HelloWorldTests: XCTestCase {

    static var allTests : [(String, (HelloWorldTests) -> () throws -> Void)] {
        return [
            ("testExample", testExample),
        ]
    }

    func testExample() {
        XCTAssertEqual(HelloWorld().text, "Hello, World!")
    }

}
struct HelloWorld {

    var text = "Hello, World!"
}

Swift 3.x in Xcode

  • More options
    • Playgrounds
    • Xcode 8.x IDE

Unit Testing 101

  • Setup and Teardown
  • Performance
  • Assertions
  • Code Coverage

Unit Testing 101

Unit Testing 101

Unit Testing 101

Unit Testing 101

Unit Testing 101

Unit Testing 101

Unit Testing 101

Unit Testing 101

Unit Testing 101

Unit Testing 101

Unit Testing 101

Tools of the Trade

  • Nimble matchers
  • Cuckoo
  • Slather
  • SwiftFormat
  • Jenkins
  • SonarQube
import XCTest
import Nimble
@testable import Calculator

class CalculatorTests: XCTestCase {

    let resCalc = CalculatorModel()

    func testAdd() {

        expect(self.resCalc.add(1,1)) == 2
    
    }

    func testAddRange() {

        expect(self.resCalc.mul(4, 3)).to(satisfyAnyOf(beGreaterThan(10),beLessThan(20)))

    }
}

Tools of the Trade

$ sudo gem install cocoapods

$ pod init

$ vi podfile

    platform :ios, '9.0'

    source 'https://github.com/CocoaPods/Specs.git'
    target 'CalculatorTests' do

    use_frameworks!
        pod 'Quick'
        pod 'Nimble', '~> 5.0.0'
    end

$ pod install

Tools of the Trade

Tools of the Trade

1. Create your project with model class and test class.

2. Run pod init

3. Edit the generated podfile and add pod “Cuckoo” as a test target.

4. Run pod install.

5. Close the project and reopen the workspace.

6. Click on the project folder then choose Test Target Build Phases.

7. Click + and choose New Run Script Phase.

8. Add Listing to the Run Script section, making sure to modify the input files that you want to mock.

9. Build the project.

10. Run the tests.

11. Drag and drop GeneratedMocks.swift into the test section.

12. Run the mocked tests.

platform :ios, '9.0'
use_frameworks!

target 'DateWithCuckooTests' do
    
    pod 'Cuckoo',
    :git => 'https://github.com/SwiftKit/Cuckoo.git',
    :branch => 'swift-3.0'
end

Tools of the Trade

Tools of the Trade

# Define output file; change "${PROJECT_NAME}Tests" to your test's 
root source folder, if it's not the default name

OUTPUT_FILE="./${PROJECT_NAME}Tests/GeneratedMocks.swift"
echo "Generated Mocks File = ${OUTPUT_FILE}"

# Define input directory; change "${PROJECT_NAME}" to your project's root
source folder, if it's not the default name
INPUT_DIR="./${PROJECT_NAME}"
echo "Mocks Input Directory = ${INPUT_DIR}"

# Generate mock files; include as many input files as you'd like to create mocks for
${PODS_ROOT}/Cuckoo/run generate --testable "${PROJECT_NAME}" \
--output "${OUTPUT_FILE}" \
"${INPUT_DIR}/FileName1.swift" \
"${INPUT_DIR}/FileName2.swift" \
"${INPUT_DIR}/FileName3.swift"
# ... and so forth

Tools of the Trade

Tools of the Trade

Tools of the Trade

# Mocking takes the form when(methodIsCalled).thenReturn(aValue)

stub(mock) {
    (mock) in when(mock.getDate.get).thenReturn(dateAndTime.from(year: 2014, month: 05, day: 20) as Date)
}

XCTAssertEqual(mock.getDate, dateAndTime.from(year: 2014, month: 05, day: 20) as Date)
XCTAssertNotNil(verify(mock).getDate)

Tools of the Trade

Tools of the Trade

$ gem install slather

$ slather coverage --html --scheme XcodeSchemeName path/to/project.xcodeproj

$ slather coverage --html --scheme Calculator Calculator/Calculator.xcodeproj

Tools of the Trade

# SwiftLint

$ brew install swiftlint

$ cd /path/to/XCode directory

$ swiftlint autocorrect

# SwiftFormat

$ brew install swiftformat

$ swiftformat --indent 4

Tools of the Trade

Tools of the Trade

XCUI

  • User Interface testing
  • Two options
    • Recorded tests
    • Roll your own

XCUI

XCUI

XCUI

XCUI

func testExample() {
    
    let app = XCUIApplication()
    app.buttons["3"].tap()
    app.buttons["*"].tap()
    app.buttons["4"].tap()
    app.buttons["="].tap()
    
    let resultTextField = app.textFields["resultsFld"]
    XCTAssert(resultTextField.value! as! String == "12")
    
}

XCUI

XCUI - Roll your own

  • XCUI has 3 component parts
    • XCUIApplication 
    • XCUIElement 
    • XCUIElementQuery  

XCUI - Roll your own

  • XCUIApplication 
    • Launch the app
  • XCUIElementQuery  
    • Find the XCUIElement to test
  • XCUIElement 
    • Perform test
class CalculatorUITests: XCTestCase { 
        
 
    override func setUp() { 
        super.setUp() 
         
        // stop if any test fails 
        continueAfterFailure = false  
    } 

    func testExample() {
        
        // Arrange
        let app = XCUIApplication()

        // Act
        app.buttons["3"].tap()
        app.buttons["*"].tap()
        app.buttons["4"].tap()
        app.buttons["="].tap()

        // Assert
        XCTAssert(app.textFields["resultsFld"].value! as! String == "12")
            
    }

} 

Books

Contact Details

@riisllc, @godfreynolan

godfrey@riis.com

http://riis.com/blog

AgileSwift

By godfreynolan

AgileSwift

  • 1,323