xUTPの著者から教えてもらった「テストのクラフトマンシップ」
はじめに
これはRecruit Engineers Advent Calendar 2016の11日目の記事ですが遅れての投稿です。すみません。 先日、xUTPの著者のセッションに行って聞いたきたので、その内容をまとめたものです。
謝辞と注意点
この記事を書くに際し、セッションのスピーカーであるGerard Meszarosさんに、特別にスライド内のコードの利用を認めていただきました*1。この場を借りて感謝いたします。Thank you for giving me the right to use code sample in Agile Singapore 2016 presentation, Mr. Meszaros!
上記の経緯から、以下に出てくるコードのライセンスはCopyright 2016 Gerard Meszaros
です。ご注意ください。
Unit Test Craftmanship概略
スピーカーについて
Gerard Meszarosさんです。 この人は『xUnit Test Patterns: Refactoring Test Code』という本*2の著者として有名な人です。
セッションの資料
- セッション概略: https://confengine.com/agile-singapore-2016/proposal/2771/unit-test-craftsmanship
- スライド: http://singapore2016.xunitpatterns.com/
- 動画: https://www.youtube.com/embed/qdSns9BOFrM?feature=oembed
本編
みんな「上手く」テストしてる?
アイスブレイク的にGerardさんが問いかけます。 「ユニットテストしてる人てをあげてー!」 個人的にはここですごく驚いたのですが、聴衆の9割近くが手をあげてました。恐るべしシンガポール。しかしGerardさんは続けます。
「上手にできてる人は?」
ここで手が挙がる人は1割くらい。みんなユニットテストでは苦労してるんですね。 というわけで、ここから上手なテストをするためのクラフトマンシップの話が始まります。
変更に強いテストを作るには?
プログラミングの経験と、xUnit(例えばJUnitみたいな)の経験、それにテストの経験、これがあれば変更に強い自動テストが作れるだろうか? NOだ。それだけでは不十分。そこからメンテナンス性の高いテストを書く前段として、プロダクションコードとテストコードを比較しながら、テストコードにはどう言ったポイントが重要なのかを説きます。
個人的にとても興味深かったのは、下記の2点です。
- Reusability(再利用性)は、プロダクションコードでは重要だけれど、テストコードでは重要ではない。テストはそれぞれコンテクストがあるから、再利用は重要ではないとのこと。
- Simplicity(簡潔さ)は「Crucial(決定的)」。なぜかというと、「だってテストコードのテストはしたくないでしょ?」とのこと。そりゃごもっとも。
ちなみにこの後実例を挙げながらテストのリファクタが始まるのですが、上記の2点はリファクタする上での結構重要な指針を授けていると感じました。
実例、テストのリファクタの開始
実例として、サンプルコードを元に説明していきます。題材になったのはインボイスのサービスの、あるメソッドのテストの話です。
最初はこんな感じ。不吉な感じが漂います。
public void testAddItemQuantity_severalQuantity () throws Exception { try { // Setup Fixture final int QUANTITY = 5; Address billingAddress = new Address("1222 1st St SW", "Calgary", "Alberta", "T2N 2V2", "Canada"); Address shippingAddress = new Address("1333 1st St SW", "Calgary", "Alberta", "T2N 2V2", "Canada"); Customer customer = new Customer(99, "John", "Doe", new BigDecimal("30"), billingAddress, shippingAddress); Product product = new Product(88, "SomeWidget", new BigDecimal("19.99")); Invoice invoice = new Invoice(customer); // Exercise SUT invoice.addItemQuantity(product, QUANTITY); // Verify Outcome List lineItems = invoice.getLineItems(); if (lineItems.size() == 1) { http://singapore2016.xunitpatterns.com 14 Copyright 2016 Gerard Meszaros if (lineItems.size() == 1) { LineItem actualLineItem = (LineItem)lineItems.get(0); assertEquals(invoice, actualLineItem.getInvoice()); assertEquals(product, actualLineItem.getProduct()); assertEquals(quantity, actualLineItem.getQuantity()); assertEquals(new BigDecimal("30"), actualLineItem.getPercentDiscount()); assertEquals(new BigDecimal("19.99"), actualLineItem.getUnitPrice()); assertEquals(new BigDecimal(“69.96"), actualLineItem.getExtendedPrice()); } else { assertTrue(“Invoice should have exactly one line item“, false); } } finally { deleteObject(expectedLineItem); deleteObject(invoice); deleteObject(product); deleteObject(customer); (略)
きっと値について質問あるよね! ってなりました。
テストのリファクタリング
ここから鮮やかなテストのリファクタリングが始まります。
まずは分解する
まず、このテストがそれぞれどんなパーツをもっているのか、というのを4フェーズテストを使って分離していきます。ざっくり
- でっかいGiven(前提のセットアップ)があって
- Whenは
invoice.addItemQuantity(product, QUANTITY);
の部分だから、When we call addItemQuantity
かな - でっかいThenがある
と言った感じ。個々のステップごとに見ていきます。
Thenの改善
「鈍い」アサーションを直す
まず取りかかったのが、「鈍い(Obtuse)」アサーションを修正することでした。上記コードだと
} else { assertTrue(“Invoice should have exactly one line item“, false); }
この部分。これを
} else { fail("invoice should have exactly one line item"); }
として、はっきりとテストがFailすべき場面であることを明示しました。
ハードコードされたデータの削除
assertEquals(new BigDecimal("30"), actualLineItem.getPercentDiscount());
これはテストをもろくするので、Expected Objectを用いてハードコードされたデータをはじき出します。 下記のように。
LineItem actualLineItem = (LineItem)lineItems.get(0); LineItem expectedLineItem = newLineItem(invoice, product, QUANTITY, product.getPrice()*QUANTITY ); // 略 assertEquals(expectedLineItem.getPercentDiscount(), actualLineItem.getPercentDiscount());
これでハードコードは取り除かれましたが、今度はアサーションが冗長になってきています。
カスタムアサーションで冗長さを解決
上記のように冗長になってしまったアサーションを、カスタムアサーションを読んでシンプルにします。
LineItem actualLineItem = (LineItem)lineItems.get(0); LineItem expectedLineItem = newLineItem(invoice, product, QUANTITY, product.getPrice()*QUANTITY ); assertLineItemsEqual(expectedLineItem, actualLineItem);
Gerardさんはカスタムアサーションをよく使うって言ってました。理由としては、カスタムアサーションにはそのテストが書けるから。あとはテストする文脈だと、既製品のマッチャーよりも、コンテクストに応じて作りこんだ方が良いものになる経験が多いから、と記憶しています。これについては後でまた出てきます。
Guardアサーションでテスト内の条件を削除
さて、ここまででだいぶテストコードは綺麗になってきました。
List lineItems = invoice.getLineItems(); if (lineItems.size() == 1) { LineItem actualLineItem = (LineItem)lineItems.get(0); LineItem expectedLineItem = newLineItem(invoice, product, QUANTITY, product.getPrice()*QUANTITY ); assertLineItemsEqual(expectedLineItem, actualLineItem); } else { fail("invoice should have exactly one line item"); }
次に気になってくるのは、テスト内に入っているif分です。このif文をGuard Assertionを使って削除します。
List lineItems = invoice.getLineItems(); assertEquals("number of items",lineItems.size(),1); // ここに Guard Assertionを入れる LineItem actualLineItem = (LineItem)lineItems.get(0); LineItem expectedLineItem = newLineItem(invoice, product, QUANTITY, product.getPrice()*QUANTITY ); assertLineItemsEqual(expectedLineItem, actualLineItem);
ここまでで一旦Thenのリファクタは落ち着きます。
Givenの改善
続いてGivenの改善に進みます。まずはハードコードされたテストデータを取り除きます
ハードコードされたテストデータの削除
問題になっているのは下記の部分
final int QUANTITY = 5; Address billingAddress = new Address("1222 1st St SW", "Calgary", "Alberta", "T2N 2V2", "Canada"); Address shippingAddress = new Address("1333 1st St SW", "Calgary", "Alberta", "T2N 2V2", "Canada"); Customer customer = new Customer(99, "John", "Doe", new BigDecimal("30"), billingAddress, shippingAddress); Product product = new Product(88, "SomeWidget", new BigDecimal("19.99")); Invoice invoice = new Invoice(customer);
まずは小さいステップとして、データの作成を別メソッドに任せます
final int QUANTITY = 5 ; Address billingAddress = new Address(getUniqueString(), getUniqueString(), getUniqueString(), getUniqueString(), getUniqueString()); Address shippingAddress = new Address(getUniqueString(), getUniqueString(), getUniqueString(), getUniqueString(), getUniqueString()); Customer customer = new Customer( getUniqueInt(), getUniqueString(), getUniqueString(), getUniqueDiscount(), billingAddress, shippingAddress); Product product = new Product( getUniqueInt(), getUniqueString(), getUniqueNumber()); Invoice invoice = new Invoice(customer);
Creation Methodを使って不要な情報を隠蔽
このgetXXXX
系のメソッド、テストと無関係(Irrelevant)な情報だよね。ということで、それらを隠蔽しにかかります。
final int QUANTITY = 5; Address billingAddress = createAnonymousAddress(); Address shippingAddress = createAnonymousAddress(); Customer customer = createCustomer(billingAddress, shippingAddress); Product product = createAnonymousProduct(); Invoice invoice = new Invoice(customer);
まだ止まりません。上記のうちbillingAddress
とshippingAddress
はやっぱり不要な情報だよねということで消しにかかります。
かくして結果は下記の通り
final int QUANTITY = 5 ; Customer customer = createAnonymousCustomer(); Product product = createAnonymousProduct(); Invoice invoice = new Invoice(customer);
まだまだ。今度はcustomer
が不要や!
final int QUANTITY = 5 ; Product product = createAnonymousProduct(); Invoice invoice = createAnonymousInvoice()
ここまででGivenはシンプルになりました。
再びのThen
ここまでくると、再びThenの中が気になってきます。
LineItem expectedLineItem = newLineItem(invoice, product, QUANTITY, product.getPrice()*QUANTITY ); List lineItems = invoice.getLineItems(); assertEquals("number of items", lineItems.size(), 1); LineItem actualLineItem = (LineItem)lineItems.get(0); assertLineItemsEqual(expectedLineItem, actualLineItem);
このうち、下記の部分が、「方法論(Mechanics)が意図を隠してしまう」と言います。
List lineItems = invoice.getLineItems(); assertEquals("number of items", lineItems.size(), 1); LineItem actualLineItem = (LineItem)lineItems.get(0);
カスタムアサーションで意図を明確に
というわけで、下記のようにカスタムアサーションを入れて意図を明確化します。
assertExactlyOneLineItem(invoice, expectedLineItem );
この部分は完全にアサーションの作り込みですね。意図を明確化するために、自分でアサーションを作りこんでいく、という発想はとても気に入りました。
ここまで来てテストはこんな感じ
public void testAddItemQuantity_severalQuantity() { // Setup final int QUANTITY = 5; Product product = createAnonymousProduct(); Invoice invoice = createAnonymousInvoice(); // Exercise invoice.addItemQuantity(product, QUANTITY); // Verify LineItem expectedLineItem = newLineItem(invoice, http://singapore2016.xunitpatterns.com 35 Copyright 2016 Gerard Meszaros LineItem expectedLineItem = newLineItem(invoice, product, QUANTITY, product.getPrice() * QUANTITY); assertExactlyOneLineItem(invoice, expectedLineItem); }
元と比べると感動ですよね。
もっと意図を明確に
4フェーズテストからの気づき
ここまでテストをリファクタしてきました。ここで冒頭の方に出てきた4フェーズテストの話を再び引き合いに出します
- Setup -> Setup or Arrange
- Exercise -> Exercise or Act
- Verify -> Verify or Assert
- TearDown -> いらないはず
記述方式によって、方法論によるか、意図によるかが変わってくるという話でした。 3Aというのが参考になります。
と話が進みます。
専門用語で作り込もう
ここからはさらにテストの意図を明確化するための作り込みが始まります。 テーマは
- DSLを利用しよう
- 関連するもののみ記述しよう
です。 まずはテスト名を
public void testAddItemQuantity_severalQuantity () {
から、JUnit4を使って
@Test public void testAddItemQuantity_severalQuantity () {
からの
@Test public void addItem_severalQuantity_itemValueIsQuantityTimesProductPrice(){
合わせてThenの中も更新します。
// Then
LineItem expectedLineItem = newLineItem(invoice, product,
QUANTITY, product.getPrice() * QUANTITY);
assertExactlyOneLineItem(invoice, expectedLineItem);
継続的にリーダビリティを向上させるよう努めること というメッセージつきでした。
// Then
shouldBeExactlyOneLineItemOn(invoice,
expectedLineItem(invoice, product, QUANTITY,
product.getPrice() * QUANTITY));
ここが僕の中でセッションのハイライトでした。 まず読みやすさありき、それによってアサートは作りこんでしまう。という意図がとてもよく出ていて感動しました。
さらにGivenの中も改善します。
Product product = createIrrelevantProduct(); Invoice invoice = createIrrelevantInvoice();
からの
Product product = givenAnyProduct(); Invoice invoice = givenAnEmptyInvoice();
ここまでで、このメソッドに対するテストは完了です。 セッションはこの後関連する別テストの話に進むのですが、ここで割愛します。 こちらもとても味がある(特にDSLの作り込みのところ)ので、ご興味のある方は冒頭にリンクを貼ったスライドをご参照ください。
クロージング
Closing Thoughtsとして、下記のことを挙げてシメられました。
- Are your automated checks helping you deliver value continuously?
- あなたの自動チェックは価値を継続的に提供する助けになっていますか?
- Do they help you understand what you need to deliver?
- それらはあなたが何を提供する必要があるのかを理解する助けになっていますか?
- Are your checks helping Make Safety a Prerequiste?
- あなたのチェックは前提条件を安全にするのに役立っていますか?
- Are they making it safer to change your code?
- それらはコードをより安全に変更できるようにしていますか?
- Are they helping you Experiment and Learn Continuously?
- それらは継続的な経験や学びを得るのに役立っていますか?
- Fast feedback on impacts of code changes?
- コードの変更のインパクトに対する素早いフィードバックがありますか?
- Are you Making People Awesome by automating the checks?
- あなたは自動チェックによって、他の人々を素晴らしくしていますか?
- Happy developers and users?
- 開発者と、ユーザーどちらも幸せですか?
どれも金言ですね。
まとめにかえて
今回のセッションに参加して
- 意図を何より明確に
- 明確にするためにDSLを作り込むなどする努力を
- 何のための自動チェックかをよく考えて
- やるとしても小さいステップで細かくそして継続的にやる
というメッセージを強く受け取りました。特に明確にするためにDSLやカスタムアサーションを利用する、というポイントはとても感動&興奮しました。 普段意識していることではあるのですが、意図を明確にするためにもっとできることは沢山ある!という気づきがあったので、そこに飽くなきクラフトマンシップを燃やしていきたいと思った次第です。
12日のエントリー
遅くなってしまいすみませんでした。 12日のエントリーはakito0107さんのターン! よろしくお願いします。