パラメタライズドテストのメリット解説(Swift)

## はじめに

本記事は [Swift Advent Calendar 2019](https://qiita.com/advent-calendar/2019/swift) の14日目の記事です。
パラメタライズドテストのメリットについて解説します。

## 注意

本記事は [「YUMEMI.swift #5 ~Free Talk~」のLTで使用した資料](https://gist.github.com/uhooi/6715b32d8e936e122816e2d069a0356c) を元にしています。
LT時は [MarkdownをVimで表示した](https://speakerdeck.com/uhooi/parameterized-test-merit) ので、よかったら併せて見てください。

## 用語

本記事で使う用語を説明します。

|用語|意味|
|:--|:--|
|テストメソッド|自動テストのメソッド|
|テストコード|自動テストのコード|
|プロダクトコード|通常のソースコード。テストコードの対義語として使う|

## あるメソッドのテストケースを考える

ここから本題です。

エンジニアが働けるかどうかを判定するメソッドを用意しました。
やる気が100以上でRed Bullを持っている場合のみ働けます。

```swift
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テストメソッドとして実装することが考えられます。

```swift: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を通すようになります。

自動テストはうまく使うとプロダクトコードを安全に破壊できるので、もったいないです。

## どうやったらテストコードがわかりやすくなる？

先ほどのデメリットにも上げた通り、 __自動テストでもエクセルのように一覧になっていたら__ わかりやすくなると思いませんか？

```swift:自動テストでテストケースを一覧にする
// | 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を使って別名を付けることで、型のように扱えます。

```swift:タプルに別名を付ける
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です。
テストメソッドの全体は以下の通りです。

```swift:工夫したテストメソッドの全体
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` はソースコードの行番号を表し、これを渡すことでテストの失敗時に対象の要素でエラーになってくれます。
<img width="988" alt="スクリーンショット 2019-12-15 0.55.49.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138245/fe00888b-c04d-b34c-c907-0ef3ba355fc1.png">

ただ、エラーメッセージに変数名が出力されないため、若干わかりづらいです。
いちいち行番号を渡すのも冗長なので、このあたりをいい感じに吸収してくれるライブラリを作りたいです。

## パラメタライズドテスト

引数のみを切り替えて実行するテストを __パラメタライズドテスト__ といいます。

言語によって呼び方が異なることがあります。

| 言語 |        名前        |
| :--- | :----------------- |
| Go   | テーブル駆動テスト |
| Java (JUnit) | パラメータ化テスト |

他言語では一般的なのに、なぜかSwiftではあまり知られていません。

以下のようにメリットも多く、簡単に導入できるのでオススメです。

- テストパターンが網羅できているかひと目でわかる
- 1メソッドで済むので処理が重複せず、命名にも困らない
- テストケースを追加・変更・削除しやすい  
1行変更するのみ

## おまけ

JavaのテストフレームワークであるSpockには「Data Tables」という機能があり、より直感的にテストケースを記述できます。

```java:Spockによるテスト
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](https://qiita.com/advent-calendar/2019/swift) の14日目の記事でした。
15日目は [@freddi_](https://qiita.com/freddi_) さんの記事です。

## 参考リンク

- [Swift で ParameterizedTest をやってみた話/swift-parameterized-test - Speaker Deck  ](https://speakerdeck.com/yusukehosonuma/swift-parameterized-test)  
私がパラメタライズドテストを知ったきっかけのスライドです  
より詳細に解説されているので、ぜひ読んでください
- [yumemi-swift-5-sample/LogicSampleTests.swift at master · uhooi/yumemi-swift-5-sample](https://github.com/uhooi/yumemi-swift-5-sample/blob/master/PrameterizedTestSampleTests/LogicSampleTests.swift)  
本記事のサンプルコードです
- https://twitter.com/lovee/status/1200595966782406656?s=20  
タプルに別名を付けるのは、Twitterでアドバイスを頂きました