既存のコードをテストで保護する
前回の記事では「スプラウトクラス」(Sprout Class)という手法を紹介しました。これは、レガシーコードに機能を追加する際に、既存のコードにはほとんど手を加えず、新機能を実現するために新しいクラスを作るという手法でした。この手法の基本にあるのは、既存のコードにテストを書くことはあきらめるとしても、せめて新しく書いたコードだけはテストで保護しよう、という考え方です。
こうした手法は時間が少ない状況で新しい機能を追加する際に有効です。しかしこれを使い続ける限り、いつまで経っても既存のコードは改善しません。
そこで、既存のコードをテストで保護する手法が必要になります。『レガシーコード改善ガイド』では、こうした手法が数多く紹介されています。今回の記事では、その中のもっとも基本的な手法の1つを紹介しましょう。
テストが困難な既存コードの例
ここでは、あるスーパーマーケットの販売管理システムに存在するRegisterクラスを例に取り上げます。
このRegisterクラスは、商品データベースへのアクセス、販売金額の集計、POS画面への表示、レシートの印刷、店員のアクセス権限の管理など、たくさんの責務を持ち、数千行のコードを含んでいます。多くのレガシーコードと同様に、すでに稼働しているシステムに対して、場当たり的に仕様の変更や追加をしてきたために、モンスターのように巨大化していて、全体像を理解するのが困難な状態になっています。
以下に示すのは、このクラスのごく一部のコードです。scanメソッドは、商品のバーコードを読み取ったときに、バーコードから商品情報を取得して販売金額を集計するために呼び出されます。このscanメソッドの単体テストを用意するにはどうすればよいでしょうか。
public class Register { private int totalAmount; ... private Item getItem(String barcode) { // データベースのリソースを取得する ... Item item = ... // 商品データベースから商品情報を取得する // データベースのリソースを解放する ... return Item; } public void scan(Event event) { String barcode; // イベントからバーコードを取得する ... Item = getItem(barcode); totalAmount = ... // 合計金額を集計するための処理 } public int getTotal() { return totalAmount; // 合計金額を返す } }
そのまま実行すべきか、それともリファクタリングすべきか
このscanメソッドでは、読み取ったバーコードから商品情報を取得するために、内部メソッドのgetItemを呼び出して、商品データベースから商品情報を取得しています。このメソッドのテストを書いて実行する方法としては、次の2つが考えられるでしょう。
- このままテストコードを書き、商品データベースにアクセスする環境を用意して実行する
- まずはデータベースに対するアクセス処理を切り離すようにメソッドのリファクタリングを行い、その後でテストを書いて実行する
1.の方法では、テスト用のデータベース環境を用意し、テストを自動実行するたびにテストデータをデータベースに登録する仕組みを用意する必要があります。こうした作業は手間がかかる上に、データベース環境用のリソースやソフトウェアライセンスが必要になります。また、テストを実行するたびにデータベースにアクセスするため、単体テストとして繰り返し実行するには時間がかかりすぎるかもしれません。
2.の方法だとデータベース環境を用意しなくてもテストを書くことができます。しかし、テストがない状態でのリファクタリングはミスを犯しやすいためとても危険です。
最低限の変更で既存コードのテストを用意する
scanメソッドのテストを書く方法がもう1つあります。その方法を使えば、実際にデータベースにアクセスすることなく、かつ大規模なリファクタリングも避けることができます。手順を以下に示します。
まず、テスト用のサブクラスを定義します。
public class TestingRegister extends Register { }
次に、商品データベースにアクセスしているgetItemメソッドをテスト用メソッドでオーバーライドします。オーバーライドしたメソッドでは、テスト結果として期待する商品情報を返すようにします。ここでは、商品情報として「100円のタマゴ」を返すようにしました。
public class TestingRegister extends Register { protected Item getItem(String barcode) { if (barcode.equals("0123456")) { return new Item("タマゴ", 100); } return null; } }
このTestingRegisterクラスでは、データベースに接続せずに、バーコード情報からItemオブジェクト(商品情報)を返すようにしています。例では単純なif文ですが、テストケースに応じて、テストデータをプロパティファイルから取得するような工夫をすれば、より多くのテストに対応できそうです。
しかし、このままではコンパイルが通りません。RegisterクラスのgetItemメソッドがprivateだからです。そこで、getItem メソッドを protected に変更します。
public class Register { ... protected Item getItem(String barcode) { ... } ... }
これで、テストコードを書くことができます。テストコードは次のようになります。
public class RegisterTest extends TestCase { public void testScan() { Register register = new TestingRegister(); Event event = new Event(SCAN_BARCODE, "0123456"); register.scan(event); assertEquals(100, register.getTotal()); } }