テストと XCTest
TOC
テスト全体の話
なぜ
自動テストのメリット
- 早く実行できる
- 何度もくり返し実行できる
早いフィードバックを頻繁に受けることができる
コードの問題点にすぐに気づくことができる
- 人為的ミスの軽減
- 属人性の排除
手動テストで同じことを繰り返すとミスが起きる
当時の開発者がいなくても、仕様を把握できる / リファクタリングができる
手動テストのための自動テスト
手動テストで担保するべき範囲がある
- ユーザビリティ
- 探索的テスト
目的を与えて、テスト設計と実行を各々で繰り返すテスト
手動テストのための時間を確保するために自動テストが必要
テストピラミッド

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モック
@endumlXCTAssertion 一覧
| 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 |
| XCTAssertGreaterThanOrEqual | expression1 ≧ expression2 を期待 引数は Comparable に準拠する必要がある XCTAssertTrue( x ≧ y ) でも書けるが、失敗時のログがよりわかりやすくなる expression1: 実際の値 expression2: 期待値 | XCTAssertGreaterThanOrEqual |
| XCTAssertLessThan(expression1, expression2) | expression1 < expression2 を期待 引数は Comparable に準拠する必要がある XCTAssertTrue( x < y ) でも書けるが、失敗時のログがよりわかりやすくなる expression1: 実際の値 expression2: 期待値 | XCTAssertLessThan |
| XCTAssertLessThanOrEqual | expression1 ≦ 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) // 等しくてもOKXCTAssertLessThan
// 10 < 20
XCTAssertLessThan(10, 20)XCTAssertLessThanOrEqual
// 10 <= 20
XCTAssertLessThanOrEqual(10, 20)
XCTAssertLessThanOrEqual(10, 10) // 等しくてもOKXCTAssertThrowsError
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)
}
}