第3章:検出と分離
はじめに
実際は複数のオブジェクトが依存し合っているので、「ある特定のクラスのテスト」をやりにくい。結局、システムのほとんど全体をテストハーネスに含めることになってしまう。
クラスをテストハーネスに入れるために、依存関係を排除する必要がある。
・テストを作成するためには対象とするクラスからほかのクラスへの影響を把握する必要がある。
→他のクラスに成りすまして影響を直接検出する。
テストを整備する際に依存関係を排除する理由
依存関係の排除が必要な理由は以下の2ケース。<検出>
コードの計算した値にアクセスできないときに、それを検出するために依存関係を排除する。
検出のための方法は「協調クラスの擬装」のみ。(後述)
<分離>
コードをテストハーネスに入れて実行することすらできないとき、分離するために依存関係を排除する。
分離のための方法は25章に記されている。
協調クラスの擬装
<擬装オブジェクト(fake object)>クラスのテストを行うときに、その協調クラスになりすますオブジェクト。
あるコードだけを独立して実行し、何が行われるかを確認したい場合、たいていは他のコードに対する依存関係を排除する必要がある。
→その「他のコード」が、確認したい作業の影響を簡単に検出できるための方法だから。
そこ(確認したい作業の影響を受ける場所)を別のコードと置き換えることで変更対象部分のテストを書けるようにする。
この「別のコード」が、「擬装オブジェクト」。
書籍で取り上げられている例
■Saleクラス・scanメソッドを有する。
・バーコードを受け取り、それに紐づく商品名と値段をレジの画面に表示する。
■問題点
・正しい文字列が表示されるかの確認が困難。
→レジ画面のAPIに対する呼び出しがSaleクラスの奥深くに存在すると、確認作業は困難。
→画面に対する影響を「検知」するのが困難。
■改善後
・画面表示の責務をArtR56Displayクラスに移譲する。
package my.app; import my.app.application_if.Display; public class ArtR56Display implements Display { @Override public void showLine(String line) { //本来ならレジのAPIをたたく何かしらの処理があるはず。 System.out.println(line); } }
・ArtR56DisplayクラスはDisplayインタフェース #show を実装している。
package my.app.application_if; public interface Display { public void showLine(String line); }
・Displayインタフェースを実装したFakeDisplayクラスを作成する。
package my.app; import my.app.application_if.Display; public class FakeDisplay implements Display { private String lastLine = ""; @Override public void showLine(String line) { this.lastLine = line; } public String getLastLine() { return this.lastLine; } }
・プロダクトコードではSale #scanでArtR56Display #showを呼ぶ。
package my.app; import my.app.application_if.Display; public class Sale { private Display display; public Sale(Display display) { this.display = display; } public void scan(String barcode) { String itemName = Item.getInstance().getItemName(barcode); String price = Item.getInstance().getPrice(barcode); String itemLine = itemName + " " + price; this.display.show(itemLine); } }
・テストコードではSale #scanでFakeDisplay #showを呼ぶ。
package my.app; import static org.junit.jupiter.api.Assertions.*; import org.junit.jupiter.api.Test; class SaleTest { @Test void testDisplayAnItem() { // set up setUp(); FakeDisplay display = new FakeDisplay(); Sale sale = new Sale(display); // execute sale.scan("0001"); // check result assertEquals("item_A 1000", display.getLastLine()); } private void setUp() { Item.getInstance().addNewItem("0001", "item_A", "1000"); Item.getInstance().addNewItem("0002", "item_B", "2000"); } }
■メリット
インタフェースに依存させることで、実際の表示こそしないものの、「何が表示されるのか」「出力される内容が正しいか」を確認することができる。
ArtR56Display #showを実行(プロダクトコード)
- レジの画面に品名と値段を表示する。
FakeDisplay #showを実行(テストコード)
- 画面に出力される内容が正しいかを確認する。
まとめ
テストを整備するために依存関係の排除が必要。排除が必要になる理由には「検出」と「分離」がある。
<検出>
コードの計算した値にアクセスできないときに、それを検出するために依存関係を排除する。
検出のための方法は「協調クラスの擬装」のみ。(この章で述べられた)
<分離>
コードをテストハーネスに入れて実行することすらできないとき、分離するために依存関係を排除する。
分離のための方法は25章に記されている。
バーコードを読み取ってレジに品名と値段を表示する例としてSaleクラスを用いた説明があった。
scanメソッド内で品名と値段を取得し、それをレジ画面に出すという内容だが、それだと画面に対する影響を検知するのが困難。
→Saleが持っている品名と値段という情報が外からアクセスできない。
→依存関係の排除が必要。
改善後では画面表示の部分はArtR56Displayに責務を委託し、それの擬装オブジェクトであるFakeDisplayを定義している。
これらはDisplayインタフェースを実装したものである。
SaleクラスからはDisplayインタフェースに対する依存を持たせ、プロダクトコードではArt56RDisplayを、テストコードではFakeDisplayを呼ぶことで、「レジ画面に表示する内容が正しいかどうかの確認」を行うことができる。
(SaleTestとFakeDisplayを参照)
目で見ないとテストできない部分はテストコードに起こしようがないと思っていたが、そうではなさそうという印象。
「実際に意図した文字列がレジ画面に表示されるかを確認する」のではなく、「表示するための文字列が正しいかどうかの確認」であれば、この章で書かれている内容を使うことで確かにテストコードで確認ができる。
今回は文字列の確認だったが、例えば「画像を表示する」際は、「画像を表示するためのパスが正しいか(表示させるための画像ファイルのパスが正しいか)」をテストコードに起こせばいいという認識。