先日参加したCodeZine AcademyTDD実践講座に参加した際に、今までモヤッていたことを質問して答えていただいた内容。
質問の内容
テスト結果をpublicメソッドから確認しづらいときはどうすればよいか。オブジェクトの内部状態を変更するメソッドで戻り値がvoidのものなど。
例:"スイッチ"クラスに"オンにする()"メソッドがあるとき、内部状態としてはオン・オフを管理するフィールドを持っているが、それを直接返すようなgetterメソッドを公開していない場合
以下のようなクラスを想定しています。
/**
* スイッチクラス。スイッチはオン・オフを切り替えることができる。
*/
public class Switch {
/** オン・オフの内部状態 */
private SwitchStatus status;
/**
* スイッチをオンにする。
*/
public void turnOn() {
this.status = SwitchStatus.ON;
}
}
結論
以下の3戦略のうちのどれかを選択する
- 副作用のない、テスト容易性を高めるメソッドを追加する(getterなど)
- リフレクションを利用する
- 内部状態によって振る舞いが変わる公開メソッドで観測する
説明用に作成したサンプルソースはこちら。
1. 副作用のない、テスト容易性を高めるメソッドを追加する(getterなど)
そもそも戻り値がvoidで内部状態を確かめることができないということは、テスト容易性が低いということ。テスト容易性が低いのはTDDでは不利に働くため、純粋にテスト容易性を上げるためにテストを観測可能にするための、副作用がないメソッド(例えばgetterメソッド等)を追加し、それ経由でテストします。
// 「1. 副作用の無いテスト容易性を高めるメソッドを追加する」経由のテスト
@Test
public void turnOnTest1() {
// 実行
_switch.turnOn();
// 検証
assertEquals(_switch.getStatus(), SwitchStatus.ON);
}
ここからは私の解釈ですが、この戦略を取る場合はpackage privateとかで宣言して必要最小限の可視性にとどめておくのが良さそうです。テストのためにgetterを無闇矢鱈と開放するのは良くないので。
2. リフレクションを利用する
Javaの場合リフレクション経由でprivate変数にアクセスできるので、それを利用してテストを書くことができます。
// 「2. リフレクションを利用する」経由のテスト
@Test
public void turnOnTest2() throws ClassNotFoundException, NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
// 実行
_switch.turnOn();
Field field = _switch.getClass().getDeclaredField("status");
field.setAccessible(true);
// 検証
assertEquals(field.get(_switch), SwitchStatus.ON);
}
個人的には、リフレクション経由でのテストはできれば避けるべきかと思います。例えば内部変数名を変えただけでテストが失敗してするようになってしまいますので。
3. 内部状態によって振る舞いが変わる公開メソッドで観測する
既存のメソッドの中で、内部状態によって振る舞いが変わるメソッドがあれば、それ経由で結果を確認することができます。
// 「3. 内部状態によって振る舞いが変わる公開メソッドで観測する」経由のテスト
@Test
public void turnOnTest3() {
// 実行
_switch.turnOn();
// 検証
assertEquals(_switch.displayName(), "オン");
}
1の方法と比べるとクラスの実装を変える必要がないのは良い点です。反面、そもそも内部状態によって振る舞いが変わるメソッドがなければこの戦略を採用することは出来ませんし、テストに使用した公開メソッドの実装が変わってしまった場合や、公開メソッド自体にバグがあった場合にテスト対象のメソッドのバグを検出しづらいという欠点がありそうです。
どれを採用するかはケースバイケース
3つの戦略のうち、どれを採用するべきか、というのはケースバイケースだと思います。
開発チーム内で、どういう場合にどの戦略を採用するのか、などを認識合わせしておくと良いかなーと思いました。