Skip to content

Instantly share code, notes, and snippets.

@sunaot
Last active February 20, 2022 06:57
Show Gist options
  • Save sunaot/c761b00b79d71f6e64bb to your computer and use it in GitHub Desktop.
Save sunaot/c761b00b79d71f6e64bb to your computer and use it in GitHub Desktop.
テストを書くか書かないかの判断の話

ユニットテストでテストを書くか書かないかの判断の話

お題

メソッドの出力の結果が、true か false のどちらでも返ってくる可能性がある場合、assert 文を書く時は true の場合だけで良いのだろうか

テストとは

まず、基本の考えとしてなぜテストをするのか?というのがあります。

テストとは、エラーをみつけるつもりでプログラムを実行する過程である。(via ソフトウェアテストの技法 [Glenford J. Myers])

という言葉のとおり、最小の手間でプログラムのエラーを見つけ出そうとする試みがテストです。裏を返せば、エラーが見つかる可能性が低いのにすべてのことを試すのはテストではありません。

判断するときの論点

いくつかこれを判断するときの論点があります (Boolean に限らず、「そのテストは必要か?」と考えるときの観点ともいえますね)。

  • 同値分割
  • 実装側の複雑さと出力結果への影響
  • テストの資産価値
  • TDD としてのテストと QA のためのテスト

同値分割

テストの設計で知っておくべき考え方に同値分割と境界値分析というものがあります。くわしくは資料へ。

http://jasst.jp/archives/jasst11cedec/pdf/S2.pdf

今回の場合、メソッド単体で見たときには true と false が結果の同値クラスとして挙がってくるのでそれぞれにテストがあるとよさそうです。

実装側の複雑さと出力結果への影響

ではどんな場合でも必ずユニットテストを書くでしょうか?

たとえばこんな実装の場合は私はテストを書かないかもしれません。

class AmusementPark
  def open?(time = Time.now)
    park_calendar.open?(time)
  end
end

この場合、park_calendar オブジェクトの open? メソッドへ処理を委譲していて、(たぶん) カレンダーの日付と開園時刻、閉演時刻から判断をして結果を返します。このとき、AmusementPark の open? メソッドとしては true/false の判断自体をしているというわけではなくて、別のオブジェクトの結果をそのまま返しているだけです。呼び出し先のオブジェクトを信頼していれば、その結果がそのまま返るというところの確認にとどめて、真偽の両方を試そうとはしないと思います。

これはユニットテスト対象の実装を知っているからできる判断です。テストは一般的に外面的な振る舞いをテストしますが、ユニットテストを書くときは実装を考慮してさじ加減を決めることがよくあります。

テストの資産価値

さじ加減と書きました。テストを書くかどうかのさじ加減をどう決めるでしょうか?

その一つの視点が、「テストとして残した場合になにがどのくらい保証できるのか」というものです。「もし、今回 false のテストを書いた場合、それは実装のどの範囲についてどのくらいのロジックの正しさを保証してくれるか」という視点がユニットテストを書くかどうかの判断では大事になってきます[^1]。

private メソッドを呼んでいるのであればその中身についても考慮したテストが必要になるかもしれません。一方、他のところでユニットテストがされていたり、外部のライブラリを呼び出しているだけなのでその処理は信頼できるものだとみなせたりして、あえて繰り返してテストをするメリットが薄いかもしれません。そうしたことを考えながら、(途中では余分に書いたり不足に気付いて書き足したりをしつつ) 役に立つテストを残していくのが大切になります。

テストの資産価値」という呼び方を覚えておくといいでしょう。

TDD としてのテストと QA としてのテスト

私は TDD としてのユニットテストを書いているときはテストケースの網羅をさほど重視しません。これがないと実装が壊れたことに気付けないと感じるポイントは粗くケースに含めますが、本当にそれで十分に足りているかの検討は後回しにすることが多いです。これは、その時点ではけっきょくその実装を後で捨てる可能性があるためです。

TDD としてのユニットテストはあくまで開発のための補助線であり、途中のリズムをとるためのものであり、それがないと足がかりがなくなって不安なので打ち込む登山のハーケンのようなものです。ただ、その足場を今後も使うかはまだ不確定なものです。ある程度登りきって有効な道筋だったと確信が持てるまでは過剰にテストケースをつくってしまう時間は無駄になる恐れがあります。

ある程度実装が固まり、これでいこうと内心思えたタイミングがテストケースの網羅率を上げるタイミングです。ここでは TDD のテストではなく、QA (品質保証) としてのテストの観点に自分の頭を切り替えます。ここで初めて、同値分割や境界値分析による単機能のテスト設計をします (とくに無効同値クラスに分類される異常系の処理のためのテストを充実させることが多いです)。

以下で見られる画像のとおり、QA のテストにはさまざまな分類があるのですが、ユニットテストでは主に単機能のテストにフォーカスして書きます。ここから外れるテスト[^2]が必要になりそうなときは、その複雑な部分を局所化してやるような設計にして、その部分のための専用のテストを書くほうがいいという合図だと受けとめて設計の見直しをしたりします。

via http://itpro.nikkeibp.co.jp/article/COLUMN/20140311/542613/?ST=develop&P=3

結論

ということで、true/false の両方についてテストを書くのかの結論としては、書くときもあるし、書かないときもある、になります。


  • [^1] これは同時に「それが判断できるくらいのロジックの量にコントロールしながら実装側のプログラムを書く必要がある」ということもいえます。
  • [^2] 複雑な組合せの入力値に対するテスト (デシジョンテーブル) が必要だったり、複雑な状態管理へのテスト (状態遷移図) が必要だったり。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment