400_004 iOSアプリ ユニットテストガイドライン - stv-ekushida/ios-design-guide GitHub Wiki

目次

1.目的
2.方針
3.ディレクトリ構成
4.命名規則
5.ツール
6.テストメソッドの粒度
7.テストメソッドの構成
8.外部依存のあるテスト
9.その他

ソフトウエアの品質を高めることを目的とする。

ポイント

・前提条件が作れないテストケースを確認できる
・前提条件が作りづらいテストケースを確認できる
・再現しづらいテストケースを確認できる
・異常系のテストケースを確認できる

役割

・テスト対象が期待する結果を把握できる
・プログラム修正による影響範囲を特定できる

ユニットテストは、メソッド単位ではなく、機能ブロック単位に行うこと。
機能ブロック単位に、正常系、異常系、組み合わせの動作パターンを検証し、
不具合がないことを確認すること。

テストの種類

種類 説明
機能確認テスト 1つの機能ブロックが機能仕様どおりに動作することを確認するテスト
制御フローテスト 命令や分岐のすべてが実行されることを確認するテスト

①機能確認テスト

機能確認テストでは、同値クラステスト、境界値テストを実施すること。
なお、処理の順番を意識したテストを実施すること。

観点 対応の可否 補足説明
基本機能
状態遷移
画面遷移 - 結合テストにて実施する
設定保持/変更/反映
表示 - 結合テストにて実施する
ユーザインターフェース - 結合テストにて実施する
セキュリティ

②制御フローテスト

・命令網羅テスト(C0)
・条件網羅テスト(C1)

※コードカバレージ100%は、目指さない。
C0のコードカバレージは、XCodeの機能を利用すること。

プロダクトコードと同一のディレクトリ構成とする。

例)MVCの場合

├─ Models
├─ Views
├─ Controllers 
├─ Utils
├─ Helpers
├─ Extensions
├─ Others

なお、外部依存関係の少ないModels、Utils、Helpers、Extensionsは、
必ずユニットテストを実施するものとする。
それ以外のディレクトリは、プロジェクト方針に従うものとする。

①クラス名

クラス名は、下記の通りとする。

<プロダクトコードのクラス名>Tests

例)
テスト対象クラス:PhotoSearchAPIの場合

PhotoSearchAPITests

②メソッド名

メソッド名は、下記の通りとする。

test_<テスト対象メソッド名>_<期待する振る舞い/結果>_<前提条件>

例)
・テスト対象メソッド名:fetch
・期待する結果:1件以上の写真が見つかること
・前提条件:「東京」というキーワードで検索した場合

test_fetch_photoFoundOverOne_whenKeywordForTokyo	

(※)期待される例外には、cannotという接頭語をつけること。

例)

test_fetch_cannotPhotoFoundZero_whenKeywordForNoTarget	

ユニットテストは、XCodeに標準搭載されているXCTestを利用すること。

ライブラリ

原則、XCTestの利用を推奨するが、プロジェクト方針により、ライブラリの利用も可とする。

参考

・Quick
・GHUnit
・OCMoc

原則、1つのテストメソッドで1つの条件をテストすること。
複数のアサーションを設定すると、失敗したアサーション以降の処理が実行されないため。

良い例

   TBD

悪い例

   TBD

テストメソッドは、setUp → test◯◯(テスト対象) → tearDownの流れで実施すること。

共通の事前処理はsetUp、事後処理はtearDownで行うものとし、
他のテストに副作用のないようにすること。

なお、個別のテストメソッドの構成は、Four Phase Testに準拠するものとする。

フェーズ 説明
Setup 事前処理
Exercise 実行
Verify 検証
TearDown 事後処理
import XCTest
@testable import SampleProject

class PhotoSearchAPITests: XCTestCase {

    override func setUp() {
        super.setUp()

        // 1. Setup(共通処理)
    }

    override func tearDown() {
        super.tearDown()
        // 4. TearDown(共通処理)
    }

    func test_fetch_photoFoundOverOne_whenKeywordForTokyo() {

        // 1.Setup(個別処理)
        // 2.Exercise
        // 3.Verify
        // 4.TearDown(個別処理)
    }
}

外部依存のあるクラスやメソッドのテストは、テストダブルに準拠するものとする。
なお、同期処理のテストは、ダミーオブジェクトまたは、テストスタブを利用するものとする。
また、非同期処理のテストは、テストスパイを利用するものとする。

パターン 同期/非同期 間接入力 間接出力 補足説明
ダミーオブジェクト 同期 - どんな入力値に対しても静的データを返す
テストスタブ 同期 - 入力値に対応する静的データを返す
テストスパイ 非同期 - 予め期待する値を間接出力に設定し、その値を返す
モックオブジェクト 非同期 - 予め期待する値を間接出力に設定し、その値を返す
なお、検証はモックオブジェクト内で行う
フェイクオブジェクト 同期/非同期 プロダクトコードの簡易版機能を持つ

①テストスタブの例

    TBD

②テストスパイの例

写真検索APIに検索キーワードを入力し、検索結果を確認するテスト

SUT

import XCTest
@testable import sample_project

class PhotoSearchAPITests: XCTestCase {

    let api = PhotoSearchAPI()
    let loadable = SpyPhotoSearchAPI()

    override func setUp() {
        super.setUp()
        api.loadable = loadable
    }

    override func tearDown() {
        super.tearDown()
        api.loadable = nil
    }

    /// 「Swift」で検索したときの1件以上のデータが取得できることを確認する
    /// check: items.count > 0
    func test_fetch_200OK_WhenKeywordSwift() {

        // 1.Setup
        let exp = expectation(description: "「Swift」で検索したときのテスト")
        loadable.asyncExpectation = exp

        // 2. Exercise
        api.fetch(title: "Swift", page: 1)

        // 3. Verify
        waitForExpectations(timeout: 1) { error in
            if let error = error {
                XCTFail("waitForExpectationsエラー: \(error)")
            }

            if let result = self.loadable.result {

                switch result {
                case .normal(let result) :
                    XCTAssertTrue(result.items.count > 0)

                case .error(let error) :
                    XCTFail(error.localizedDescription)

                default:
                    XCTFail("")
                }
            }
        }
    }
}

DOC

import XCTest
@testable import sample_project

final class SpyPhotoSearchAPI: PhotoSearchLoadable {

    var result: PhotoSearchStatus?
    var asyncExpectation: XCTestExpectation?

    func setResult(result: PhotoSearchStatus) {

        guard let expectation = asyncExpectation else {
            XCTFail("")
            return
        }

        self.result = result
        expectation.fulfill()
    }
}

①その他のルール

・テストクラスとプロダクションコードは分離しましょう。
・複数のテストケースで利用する機能は、ヘルパーやユーティリティを使いましょう。

②用語集

用語 説明
SUT テスト対象を指す(System Under Test)
DOC 依存するコンポーネントを指す (Depended-On Component)
⚠️ **GitHub.com Fallback** ⚠️