仕様化テストの手順
では、宅配便の配送料金を計算するDeliveryService
というクラスを例に取って、仕様化テストを書く手順について説明しましょう。このクラスには、発送地と目的地の郵便番号、配送指定(通常、速達など)、重量、サイズを引数として配送料金を計算するcalculateDeliveryCharges
メソッドがあります。実際のコードは省略しますが、このメソッドはさまざまな条件による組み合わせを追加していった結果、巨大なメソッドとなってしまっています。このメソッドに対するテストを書いて、仕様を文書化してみましょう。
まずは基本的なケースとして、同じ地域の目的地向けに(郵便番号が同じ)、重量が1Kgでサイズが40の荷物を通常便で送る場合のテストを書いてみましょう。結果がどうなるのかは分からないので、あり得ない値(ここでは3億円としました)を設定しておきます。
public void testCalculateDeliveryCharges() { DeliveryService service = new DeliveryService(); Money charges = service.calculateDeliveryCharges( "100-0000", "100-0000", NORMAL, 1, 40); assertEquals(300000000, charges.getValue()); }
このテストを実行すると、想定した通りテストは失敗しました。テスト結果から、このテストで指定した条件だと、calculateDeliveryCharges
メソッドは配送料金として400円を返すようです。
junit.framework.AssertionFailedError: expected:<300000000> but was:<400>
テストを成功させるには、次のようなコードを書けばよいことが分かります。
public void testCalculateDeliveryCharges() { DeliveryService service = new DeliveryService(); Money charges = service.calculateDeliveryCharges( "100-0000", "100-0000", NORMAL, 1, 40); assertEquals(400, charges.getValue()); }
このテストにより、calculateDeliveryCharges
メソッドは、同じ地域内で、重量が1Kg以下で40サイズ以下の荷物を通常便で送る場合の料金は400円であるという振る舞いが確認できました。
続けて、発送地と目的地を指定せず、通常の配送指定をし、重量とサイズが0の場合についてテストを書いてみましょう。
public void testCalculateDeliveryCharges() { DeliveryService service = new DeliveryService(); Money charges = service.calculateDeliveryCharges("", "", NORMAL, 0, 0); assertEquals(300000000, charges.getValue()); }
このテストを実行すると、想定通りテストは失敗しました。引数を正しく指定しなかった場合、calculateDeliveryCharges
メソッドは、配送料金として-1円を返す仕様になっているようです。
junit.framework.AssertionFailedError: expected:<300000000> but was:<-1>
この結果を受けて、テストが成功するように変更します。
public void testCalculateDeliveryCharges() { DeliveryService service = new DeliveryService(); Money charges = service.calculateDeliveryCharges("", "", NORMAL, 0, 0); assertEquals(-1, charges.getValue()); }
仕様化テストで文書化した振る舞いを最新に保つ
このような手順を繰り返していくと、既存のコードの振る舞いを段階的に理解しながら、同時に既存のコードの振る舞いを「文書化」できます。実際に仕様書としてまとめるわけではありませんが、仕様化テストとして整備した単体テストを見れば、そのコードの振る舞いを理解できるようになるわけです。
もしかすると、仕様化テストを書くことで明らかになったコードの振る舞いは、ずっと昔に作られた仕様書とは異なるかもしれません。しかし、仕様書と異なっていても、その振る舞いこそが現在のコードとしては正しいことになります。加えて、仕様化テストのコードは実際に動かすことができるため、そこに書かれた振る舞いの正しさをいつでも検証できます。テストを頻繁に動かしてメンテナンスを行えば、文書化した振る舞いを最新の状態に保つことができます。
仕様化テストを書く手順をまとめると、次のようになります。
- ステップ1:テストコードで対象となるコードを呼び出す
- ステップ2:失敗することが分かっている表明を書く
- ステップ3:失敗からどんな振る舞いかを確認する
- ステップ4:コードが実現する振る舞いをするようにテストを変更する
- ステップ5:以上の手順を繰り返す
まずは「テストで保護する」ことに専念する
ところで先ほどの配送料金を計算するメソッドでは、引数が不正な場合には-1円を返すことが分かりました。しかし、引数が不正な場合には、例外(java.lang.IllegalArgumentException
や独自の例外クラス)を発生させるべきと考えた方もおられるかもしれません。
しかし、よく考えてみましょう。コードを「あるべき姿」に変更することはそれほど簡単ではないかもしれません。巨大化しているcalculateDeliveryCharges
メソッドを修正し、さらにこのメソッドを呼び出しているすべてのコードを修正し、正しく修正できたことを確認するためのテストを行う必要があります。もしかすると、作業途中に見つけたバグの修正や別のリファクタリングをしたくなるかもしれません。こうした作業を行うのは良いことですが、テストなしに行うのには大きなリスクがあります。
先ほど書いたテストは、システムを変更する準備作業として、コードを十分理解するために書いたものです。コードを「あるべき姿」に変更することより、まずは現状を受け入れてテストを整備することが重要です。仕様化テストでコードの振る舞いを理解し、テストを整備できれば、自信を持って安全にリファクタリングを始めることができます。
システムに変更を加える場合、当てずっぽうで変更を加えるわけにはいきません。しかし、限られた時間では、コードを隅々まで理解することも大変です。そのような状況では、仕様化テストを整備することを試してみてください。仕様化テストが整備できれば、既存のコードの振る舞いを「保護する」ことができるようになり、安心して変更を加えることができるようになるはずです。