テストと 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モック
@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 |
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) // 等しくても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)
}
}