たにしきんぐダム

プログラミングやったりゲームしてます

Tests as Documentation

production code の設計についてはよく議論される一方、ユニットテストをどう書くべきかについてはあまり議論されることが少なく。とにかくカバレッジが高ければヨシみたいな感じで軽く扱われていることが多い気がする。

その結果、テストを書くときやとりわけテストを追加するときに "良くない" 方法でテストを追加/拡張してしまい、メンテナンスしにくく壊れやすい・(未来の自分でも)読んでも何を検証しているのか分からない、テストが落ちても不安だけを煽り何が問題なのか分からない、技術的負債が誕生してしまう。

詳しいことは本 ( XUnit Test Patterns など? 詳しい人は僕に紹介してください)を読んだりチームメンバーと議論するのが良いと思うが、この記事を読んでテストの書き方に対する意識を啓発できたらなと思っている。

理想を述べるのは簡単だけど現実は大変、頑張ろう


introduction

ユニットテストの役割のうち最も重要な事のひとつは、書いた・変更したコード(ユニット)が"期待された振る舞い"をすることを検証すること。これに加えて個人的に最も重要な役割だと信じているものが「ドキュメントとしてのユニットテスト」で、xUnit Test Patterns では Communicate Intent と呼ばれる原則として紹介されている。

ユニットテストは理想的には、あるプログラムユニット(関数やクラス)がどういう条件下で・どういう入力に対して・どういう出力を返すか(・またはモックライブラリを使ってどういう挙動をするか)を検証するように書かれており、"良い"ユニットテスト(意図が掴みやすいテスト)は

  • それを読むことでSUT(system under test)の期待する振る舞いの全体像を早く・正確に捉えることができる。
    • (プログラムを一行一行読んだり、更新されてるか(もしくは存在するかも)わからない仕様書と比べて)
  • メンテナンスしやすい
    • 複雑なコードより、わかりやすい簡素なコードのほうがメンテナンスしやすいのと同じですね

そのようなユニットテストを書くために何を気をつけるべきか、何をしないべきか


意図が伝わりやすいテストを書くために

Tests should be easy to write and maintain - Goals of Test Automation によくまとまっている。

Simple Test

テストは小さく、ひとつにつきひとつの条件をテストする Verify One Condition per Test という原則を満たすようにするべき。これによりそのテストが何を検証しているのかが分かりやすくなり、テストが落ちた時に Defect Localization が容易になる。

  • 必ずひとつのテストに一つの assertion にするべきというものではなく、ひとつのテストはひとつの "挙動" を検証するべきというもの。
    • 挙動の検証に複数の assertion が必要なら無理にテストを分割する必要はない
  • また条件と期待する結果の対応がはっきりしているなら Table Driven Testsi などを使うと良いかもしれない。

Expressive Tests

Test Utility Method などを利用して、テストを読むことで何が起きているのか・何を検証しようとしているのか分かりやすいプログラムにしましょうというもの。プロダクションコードに対しては当たり前によく言われていることですね。

Test Utility Method の欠点として、test reader が知るべきAPIが増えてしまうことがありますが、例えば assertContainsExactlyOneLineItem のような Intent Revealing Name を Test Utility Method につけてあげることで、意味の伝わりやすいテストを書くことができる。

Separation of Concerns

  • Keep Test Logic Out of Production Code
    • できる限りテストのために production code に back door のようなものを仕込んだりしないようにしましょう
    • テストとは、システムの挙動を検証するもので、テスト中にシステムの挙動が変わってしまっているのなら、production code をテストしていることにはならない。
  • Test Concerns Separately
    • 例えば(ユニットテストで)UIのテストの中でビジネスロジックをテストしてはいけない。
      • テスト対象の関係性が変更されるたびに、テストが壊れてしまう(ビジネスロジックの変更によりUIのテストが壊れるなど)。これにより、何が原因でテストが壊れたのか問題を分離することが難しくなる。
      • 簡単なように思えるが、システムが複雑で返ってきた値を何でもかんでもassertしているといつの間にか依存モジュールの挙動に左右されるテストが出来上がってしまうことは往々にしてある。SUTが真に成し遂げようとしている振る舞いを分析して最小限のテストを頑張って書こう。難しいけど。
    • うまく Test Double を使いましょう

アンチパターン

上の項と多少内容が重複するが、xUnit Test Patterns では(意図や挙動が)分かりにくいテストのことを Obscure Test と読んでいて、そのいくつかの原因に関する考察が紹介されている。

Eager Test

  • とにかく一つのテストで何でもかんでも assert しまくってしまっているテスト
  • Verify One Condition per Test 原則に従いましょう

Mystery Guest

  • 暗黙的な事前条件(test reader が気づくことができない条件)がテストの結果に影響しているおり、テストを読んでもどういう条件下でその出力が得られるのか分かりにくい状態
  • 例えば、外部ファイル・他のテストなどによって追加されるDBのデータ・ランダムに生成されたデータ
    • fake dataに意図せずテストが依存してしまうことはよくあるし気づきにくい。個人的にはfake dataの利用は局所的にしていきたいと思っている(例えばテストデータのオブジェクトそのものを生成するのではなく、オブジェクトのうち関心のないフィールドに対してだけ利用するなど)

General Fixture

  • ひとつの Shared Fixture がtoo manyテストのために働いている状態
  • Mystery Guest と同じように、巨大な Shared Fixture が設定したデータにテストが依存しており test reader がテストを読んでもどういう条件下でその出力が得られるのか分かりにくい状態
  • Test fixture は Minimal Fixture を目指すべき

Irrelevant Information

  • テストの入出力に、SUTの挙動に関係のないデータが大量に記述されている状態
  • SUT の動作に本当に影響を与えるものがどれか分かりにくい、どのassertionがこのSUTが満たすべき振る舞いを検証しているのか分かりにくい
  • constructor や factory method への直接呼び出しを、関連情報のみをパラメータとするCreation Methodの呼び出しに置き換えよう
    • テストにとって重要でない値は、Creation Methods の中でデフォルト化 or ダミーオブジェクトに置き換えて隠蔽する。こうすることで、test reader に対して「表示されない値は、期待される結果には影響しません」と伝えることができる。

Hard-Coded Test Data

例えば以下の例では、30 とか 19.9 のような hard coded された数字を入力し、結果として 69.96 などの値を期待しているが、これが何を意図した値なのかが分からない (69.96 はどこから来たんだ?)

we might still miss the relationship between the unit price (19.99), the item quantity (5), the discount (30%) and the total price (69.96.)

   public void testAddItemQuantity_severalQuantity_v12(){
      //  Setup Fixture
      Customer cust = createACustomer(new BigDecimal("30"));
      Product prod = createAProduct(new BigDecimal("19.99"));
      Invoice invoice = createInvoice(cust);
      // Exercise SUT
      invoice.addItemQuantity(prod, 5);
      // Verify Outcome
      LineItem expected = new LineItem(invoice, prod, 5,
            new BigDecimal("30"), new BigDecimal("69.96"));
      assertContainsExactlyOneLineItem(invoice, expected);
   }

http://xunitpatterns.com/Obscure%20Test.html から引用

意味が伝わりやすい定数にしてあげたり、無関係な値は Creation Method によって隠蔽してあげるなどしましょう。

Indirect Testing

  • プレゼンテーション層などの中間オブジェクトを通してビジネスロジック(SUT)をテストすることなど
  • 中間オブジェクトを通してSUTの挙動を観察することになるので、入出力が複雑になり分かりにくい。
  • 原因としては product code がテストしにくい設計になっている可能性が高い。

参考