hira22

テストと XCTest

TOC

  1. テスト全体の話
    1. なぜ
    2. テストピラミッド
    3. でも時間だってない
  2. テストの実行
    1. 全てのテストを実行する
    2. 対象を絞って実行する
    3. ランダムに実行する
  3. 単体テスト
    1. 目的
    2. 注意点
    3. XCTAssertion 一覧
    4. コードサンプル
    5. 非同期処理のテスト(XCTestExpectation)
  4. 参考

テスト全体の話

なぜ

自動テストのメリット

  • 早く実行できる
  • 何度もくり返し実行できる
早いフィードバックを頻繁に受けることができる
コードの問題点にすぐに気づくことができる
  • 人為的ミスの軽減
  • 属人性の排除
手動テストで同じことを繰り返すとミスが起きる
当時の開発者がいなくても、仕様を把握できる / リファクタリングができる

手動テストのための自動テスト

手動テストで担保するべき範囲がある

  • ユーザビリティ
  • 探索的テスト
    目的を与えて、テスト設計と実行を各々で繰り返すテスト
手動テストのための時間を確保するために自動テストが必要

テストピラミッド

The Forgotten Layer of the Test Automation Pyramid

種類内容テスト量
単体テスト一つのクラス・構造体などに対して行うテスト
結合テスト複数のモジュールを組み合わせて行うテスト
UI テストUI を操作して行うテスト

ユーザーと同じ操作ができる UI テストを網羅しておけば、全ての機能の品質を担保できるように思えるが、コストがかかる。

  • テストの実行時間が長い
  • テストが壊れやすい

逆ピラミッド型になっていると、テストは脆く崩れやすいと言われている。(アンチパターン)

でも時間だってない

テストのメリットはわかるが…

  • プロダクトコード以外にテストコードを実装する必要がある
  • UI や機能を変更したら、テストコードもメンテナンスしないといけない
  • テストが失敗したら、自分が変更した箇所以外も調査・修正しなければいけない

テストを捨てる勇気も必要

  • 全ての機能を網羅しようとしない
  • コードを書いているときに、不安になる箇所だけテストを書く(不安駆動チェック

もうプロダクトコードがあるときには…

変更が少なく、重要な機能を優先してテストを実装する

  • メンテナンス画面
  • アップデート通知

テストの実行

全てのテストを実行する

⌘ + U

対象を絞って実行する

エディタを使う

◇ をクリック
TestCase 単位や Test 単位でのテストの実行を行うことができます。

// TODO: add Image

テストナビゲータを使う

テストナビゲータを表示し、▷ をクリック

// TODO: add Image

ランダムに実行する

複数のテストが依存していないことを確認するためには、ランダムにテストを実行する方法があります。

// TODO: add Image

手順

単体テスト

目的

クラスや構造体などのある 1 つの部品(ユニット)に対するテスト

注意点

ほとんどの場合、あるクラスは別のクラスに依存している。
例えば、Controller や View は Model に依存している。
Controller や View のテストをするためには、Model が必要になる。
しかし、依存しているクラスを含めてテストすると、テスト対象が不明確になる。

@startuml
Test対象 <.. Test対象が依存しているクラス
Testコード <-left- Test対象:使用
@enduml

モック(テストダブル)

この場合、モック(テストダブル)という手法を 使う。
Model をテスト用の偽物のコードに置き換えて、対象のクラスや構造体だけをテストする。

@startuml
 interface Test対象が依存 {}
 
 Test対象 <.. Test対象が依存
 
 Test対象 <|- Testコード:使用
 
 Test対象が依存 <|.d. Test対象が依存しているクラス
 
 Test対象が依存 <|.d. Testモック
@enduml

XCTAssertion 一覧

Assert説明コード例
XCTFailテストを失敗させる。
テストが正しく行われていないケースに入っている場合は失敗させる。
XCTFail
XCTAssertNil結果が nil であることを期待XCTAssertNil
XCTAssertNotNil結果が nil でないことを期待XCTAssertNotNil
XCTAssertEqual(expression1, expression2)expression1 と expression2 が一致することを期待
引数は Equatable に準拠する必要がある
expression1: 実際の値
expression2: 期待値
XCTAssertEqual
XCTAssertNotEqual(expression1, expression2)expression1 と expression2 が一致しないことを期待
引数は Equatable に準拠する必要がある
expression1: 実際の値
expression2: 期待値
XCTAssertNotEqual
XCTAssertTrue結果が true であることを期待
XCTAssertEqual(expression1, true) でも書けるが、失敗時のログがよりわかりやすくなる
XCTAssertTrue
XCTAssertFalse結果が false であることを期待
XCTAssertEqual(expression1, false) でも書けるが、失敗時のログがよりわかりやすくなる
XCTAssertFalse
XCTAssertGreaterThan(expression1, expression2)expression1 > expression2 を期待
引数は Comparable に準拠する必要がある
XCTAssertTrue( x > y ) でも書けるが、失敗時のログがよりわかりやすくなる
expression1: 実際の値
expression2: 期待値
XCTAssertGreaterThan
XCTAssertGreaterThanOrEqualexpression1 ≧ expression2 を期待
引数は Comparable に準拠する必要がある
XCTAssertTrue( x ≧ y ) でも書けるが、失敗時のログがよりわかりやすくなる
expression1: 実際の値
expression2: 期待値
XCTAssertGreaterThanOrEqual
XCTAssertLessThan(expression1, expression2)expression1 < expression2 を期待
引数は Comparable に準拠する必要がある
XCTAssertTrue( x < y ) でも書けるが、失敗時のログがよりわかりやすくなる
expression1: 実際の値
expression2: 期待値
XCTAssertLessThan
XCTAssertLessThanOrEqualexpression1 ≦ expression2 を期待
引数は Comparable に準拠する必要がある
XCTAssertTrue( x ≦ y ) でも書けるが、失敗時のログがよりわかりやすくなる
expression1: 実際の値
expression2: 期待値
XCTAssertLessThanOrEqual
XCTAssertThrowsError(expression, errorHandler)expression で例外が発生することをチェックする
errorHandler 内で例外の内容を検証できる
XCTAssertThrowsError
XCTAssertNoThrow例外が発生しないことを期待XCTAssertNoThrow

コードサンプル

XCTFail

func testMethod() {
    XCTFail()
}

XCTAssertNil

let notNumber = Int("Hello") // 数値に変換できずnil
XCTAssertNil(notNumber)

XCTAssertNotNil

let number = Int("42") // 数値に変換できInt
XCTAssertNotNil(number)

XCTAssertEqual

let string = "Hello"
XCTAssertEqual(string, "Hello") // "Hello"と等しい

XCTAssertNotEqual

let string = "Hello"
XCTAssertNotEqual(string, "Goodbye") // "Goodbye"と等しくない

Equatable に準拠する

// プロダクトコード
struct User {
    let name: String
    let age: Int
}
// テストコード
class UserTests: XCTestCase {
    func testInit() {
        let actual = User(name: "foo", age: 10)   // 実際の値
        let expected = User(name: "foo", age: 10) // 期待値
        XCTAssertEqual(actual, expected)
    }
}
 
// プロダクトコードを変更せずに、テストコードのみ Equatable に準拠することも可能
extension User: Equatable {
    static func ==(lhs: User, rhs: User) -> Bool {
        return lhs.name == rhs.name && lhs.age == rhs.age
    }
}

XCTAssertTrue

let string = "Hello"
XCTAssertTrue(string.hasPrefix("He")) // "He"から始まる

XCTAssertFalse

let string = "Hello"
XCTAssertFalse(string.isEmpty) // 空ではない

XCTAssertGreaterThan

// 20 > 10
XCTAssertGreaterThan(20, 10)

XCTAssertGreaterThanOrEqual

// 20 >= 10
XCTAssertGreaterThanOrEqual(20, 10)
XCTAssertGreaterThanOrEqual(20, 20) // 等しくてもOK

XCTAssertLessThan

// 10 < 20
XCTAssertLessThan(10, 20)

XCTAssertLessThanOrEqual

// 10 <= 20
XCTAssertLessThanOrEqual(10, 20)
XCTAssertLessThanOrEqual(10, 10) // 等しくてもOK

XCTAssertThrowsError

XCTAssertThrowsError(try throwError()) // throwError()がなんらかの例外をスローすることを期待
enum APIError: Error {
    case sampleError // なにかしらのエラー
    case anotherError  // 他のエラー
}
例外の内容を検証する
// プロダクトコード
struct SampleModel {
    // 例外をスローする可能性がある API リクエスト
    static func fetch() throws {
        // API リクエストの処理...
        if httpStatusCode == 400 {
            throw APIError.anotherError
        }
        if httpStatusCode == 500 {
            throw APIError.sampleError
        }
    }
}
// テストコード
class SampleModelTests: XCTestCase {
    func testFetch() {
        XCTAssertThrowsError(try SampleModel.fetch()) { (error: Error) -> Void in
            // スローされた例外が APIError.sampleError であること
            XCTAssertEqual(error as? DownloadError, APIError.sampleError)
            // XCTAssertTrue(error! is APIError.sampleError)
        }
    }
}

XCTAssertNoThrow

XCTAssertNoThrow(try noThrowError()) // noThrowError()がなにも例外をスローしないことを期待

非同期処理のテスト(XCTestExpectation)

非同期処理をテストする場合は、 XCTestExpectation を利用し、処理が完了するまで待機する。
処理の完了を待機していないと、テストが意図せず成功/失敗してしまう。

// プロダクトコード
struct SampleModel {
    // 例外をスローする可能性がある API リクエスト
    static func fetch(@escaping completion: (Result<User, Error>) -> Void) throws {
        // API リクエストの処理...
        // 結果を非同期に返却
        DispatchQueue.main.async {
            completion(.success(user))
        }
    }
}
 
struct User {
    let name: String
    let age: Int
}
// テストコード
class SampleModelTests: XCTestCase {
    func testFetch() {
        // 処理を待機させる
        let exp: XCTestExpectation = expectation(description: "wait for finish")
 
        SampleModel.fetch { (result: Result<User, Error>) in
            switch result {
            case .success(let data):
                XCTAssertEqual(data.agentId, 123)
            case .failure(let error):
                XCTFail("error: \(error)")
            }
 
            // expの待機を解除
            exp.fulfill()
        }
 
        // exp.fulfill()が実行されるまで、5秒間待機する
        wait(for: [exp], timeout: 5)
    }
}

参考