10
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

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

はじめに

本記事は 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 を境界値分析すると、 99100 が境界値とわかります。
さらに「100未満」と「100以上」で同値分割できるので、 99100 の2通りのみテストすれば十分です。

hasRedBull はBool型なので truefalse の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テストメソッドとして実装することが考えられます。

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 はソースコードの行番号を表し、これを渡すことでテストの失敗時に対象の要素でエラーになってくれます。
スクリーンショット 2019-12-15 0.55.49.png

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

パラメタライズドテスト

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

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

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

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

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

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

おまけ

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

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 の14日目の記事でした。
15日目は @freddi_ さんの記事です。

参考リンク

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
10
Help us understand the problem. What are the problem?