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の利用を推奨するが、プロジェクト方針により、ライブラリの利用も可とする。
原則、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に検索キーワードを入力し、検索結果を確認するテスト
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("")
}
}
}
}
}
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) |