はじめに
本記事は Swift Advent Calendar 2019 の14日目の記事です。
パラメタライズドテストのメリットについて解説します。
注意
本記事は 「YUMEMI.swift #5 ~Free Talk~」のLTで使用した資料 を元にしています。
LT時は MarkdownをVimで表示した ので、よかったら併せて見てください。
用語
本記事で使う用語を説明します。
用語 | 意味 |
---|---|
テストメソッド | 自動テストのメソッド |
テストコード | 自動テストのコード |
プロダクトコード | 通常のソースコード。テストコードの対義語として使う |
あるメソッドのテストケースを考える
ここから本題です。
エンジニアが働けるかどうかを判定するメソッドを用意しました。
やる気が100以上でRed Bullを持っている場合のみ働けます。
func canWork(motivation: Int, hasRedBull: Bool) -> Bool {
if motivation < 100 {
return false
}
if !hasRedBull {
return false
}
return true
}
こちらのメソッドのテストケースは全部で何パターン必要でしょうか?
あるメソッドのテストケース数
私が考える正解は、以下の4パターンです。
motivation | hasRedBull | return value |
---|---|---|
99 | true | false |
99 | false | false |
100 | true | true |
100 | false | false |
motivation
を境界値分析すると、 99
と 100
が境界値とわかります。
さらに「100未満」と「100以上」で同値分割できるので、 99
と 100
の2通りのみテストすれば十分です。
hasRedBull
はBool型なので true
と false
の2通りしかありません。
2通り × 2通り = 4パターン です。
手動でテストする場合
上記のメソッドを 手動で テストする場合、エクセルなどを使って一覧表に○×を付けるのが一般的だと思います。
先ほどの表に「No」「判定」「日付」「担当」列を追加しました。
No | motivation | hasRedBull | return value | 判定 | 日付 | 担当 |
---|---|---|---|---|---|---|
1 | 99 | true | false | OK | 2019/12/14 | ウホーイ |
2 | 99 | false | false | OK | 2019/12/14 | ウホーイ |
3 | 100 | true | true | NG | 2019/12/14 | ウホーイ |
4 | 100 | false | false | OK | 2019/12/14 | ウホーイ |
これで十分であり、表になっているのでパターンが網羅できているかもわかりやすいです。
自動でテストする場合
上記のメソッドを 自動で テストする場合はどうでしょうか?
最も簡単な方法として、1ケース1テストメソッドとして実装することが考えられます。
func test_canWork_motivation_99_hasRedBull_true() {
let logic = LogicSample()
XCTAssertFalse(logic.canWork(motivation: 99, hasRedBull: true))
}
func test_canWork_motivation_99_hasRedBull_false() {
let logic = LogicSample()
XCTAssertFalse(logic.canWork(motivation: 99, hasRedBull: false))
}
func test_canWork_motivation_100_hasRedBull_true() {
let logic = LogicSample()
XCTAssertTrue(logic.canWork(motivation: 100, hasRedBull: true))
}
func test_canWork_motivation_100_hasRedBull_false() {
let logic = LogicSample()
XCTAssertFalse(logic.canWork(motivation: 100, hasRedBull: false))
}
テストケースごとにテストメソッドを実装することがあるかもしれませんが、実は以下のデメリットがあります。
網羅できているかわかりづらい
エクセルのようにテストケースが一覧になっていないので、網羅できているかわかりづらいです。
同じような処理で読みづらい
canWork()
メソッドに渡す引数以外は同じ処理なので、どこが違うか注力して読み比べる必要があります。
同じ処理を繰り返すのはDRY原則にも反しています。
メソッド名に悩む
テストメソッドの命名はメソッド数が増えるほど難しくなり、 testCanWork2()
のように末尾に連番を振ることがあるかもしれません。
今回は引数と値をそのままメソッド名に付けましたが、適切ではない気がします。
テストコードがわかりづらいとどうなるか
テストコードの可読性が低い場合、 テストコードが保守されなくなる 可能性が高まります。
テストコードはプロダクトコードに比べて可読性が無視されがちですが、それだとテストが失敗しても直せなくなり、テストが成功しなくてもPRを通すようになります。
自動テストはうまく使うとプロダクトコードを安全に破壊できるので、もったいないです。
どうやったらテストコードがわかりやすくなる?
先ほどのデメリットにも上げた通り、 自動テストでもエクセルのように一覧になっていたら わかりやすくなると思いませんか?
// | motivation | hasRedBull | return value |
// | ---------: | :--------- | :----------- |
// | 99 | true | false |
// | 99 | false | false |
// | 100 | true | true |
// | 100 | false | false |
//
// ↓
//
let testCases: [(motivation: Int, hasRedBull: Bool, expect: Bool)] = [
( 99, true, false),
( 99, false, false),
(100, true, true ),
(100, false, false)
]
どうでしょうか?
テストケースをタプルで表して配列に入れることで、エクセルのように一覧で見れるようになりました。
タプルはtypealiasを使って別名を付けることで、型のように扱えます。
typealias TestCase = (motivation: Int, hasRedBull: Bool, expect: Bool)
let testCases: [TestCase] = [
( 99, true, false),
( 99, false, false),
(100, true, true ),
(100, false, false)
]
ちょっとした工夫ですが、より可読性が上がりました。
構造体を定義するより簡単に使えるので、型を使い捨てる場合には便利です。
あとは作成した配列をループさせ、1つずつアサーションすればOKです。
テストメソッドの全体は以下の通りです。
func test_canWork() {
typealias TestCase = (line: UInt, motivation: Int, hasRedBull: Bool, expect: Bool)
let testCases: [TestCase] = [
(#line, 99, true, false),
(#line, 99, false, false),
(#line, 100, true, true ),
(#line, 100, false, false)
]
for (line, motivation, hasRedBull, expect) in testCases {
let logic = LogicSample()
let result = logic.canWork(motivation: motivation, hasRedBull: hasRedBull)
XCTAssertEqual(result, expect, line: line)
}
}
#line
はソースコードの行番号を表し、これを渡すことでテストの失敗時に対象の要素でエラーになってくれます。
ただ、エラーメッセージに変数名が出力されないため、若干わかりづらいです。
いちいち行番号を渡すのも冗長なので、このあたりをいい感じに吸収してくれるライブラリを作りたいです。
パラメタライズドテスト
引数のみを切り替えて実行するテストを パラメタライズドテスト といいます。
言語によって呼び方が異なることがあります。
言語 | 名前 |
---|---|
Go | テーブル駆動テスト |
Java (JUnit) | パラメータ化テスト |
他言語では一般的なのに、なぜかSwiftではあまり知られていません。
以下のようにメリットも多く、簡単に導入できるのでオススメです。
- テストパターンが網羅できているかひと目でわかる
- 1メソッドで済むので処理が重複せず、命名にも困らない
- テストケースを追加・変更・削除しやすい
1行変更するのみ
おまけ
JavaのテストフレームワークであるSpockには「Data Tables」という機能があり、より直感的にテストケースを記述できます。
class MathSpec extends Specification {
def "maximum of two numbers"(int a, int b, int c) {
expect:
Math.max(a, b) == c
where:
a | b | c
1 | 3 | 3
7 | 4 | 7
0 | 0 | 0
}
}
Swiftでは、独自演算子とFunction Builderを使って実現できそうな気もします。
強い方教えてください…。
おわりに
パラメタライズドテストのメリットが少しでも伝わっていると嬉しいです😊
以上、 Swift Advent Calendar 2019 の14日目の記事でした。
15日目は @freddi_ さんの記事です。
参考リンク
-
Swift で ParameterizedTest をやってみた話/swift-parameterized-test - Speaker Deck
私がパラメタライズドテストを知ったきっかけのスライドです
より詳細に解説されているので、ぜひ読んでください -
yumemi-swift-5-sample/LogicSampleTests.swift at master · uhooi/yumemi-swift-5-sample
本記事のサンプルコードです -
https://twitter.com/lovee/status/1200595966782406656?s=20
タプルに別名を付けるのは、Twitterでアドバイスを頂きました