JUnitにおけるリファクタリングの意義
JUnitにおけるリファクタリングは、テスタビリティの向上が至上命題となる。
保持する状態が複雑なクラス、引数を多く持つメソッド、責務が広いメソッドなどは、
検証が難しい複雑なテスト対象となる。
他にも、実装がまだ完了していない、サービスが停止していて、テスト対象が依存している
外部Webサービスが使用できないなど、ユニットテストが実行できない状況などもある。
JUnitにおけるリファクタリングでは、こういったテストケースのテスタビリティを
改善するために、テスト対象の振る舞いは変えずに内部構造のみを変える。
テスタビリティとは
テスタビリティとは、テストのしやすさである。
一般に、実行タイミング、実行環境、実行順序などの
外部環境に影響されるテストケースはテスタビリティが低いとされる。
リファクタリングとは
前述の通り、リファクタリングとはテスト対象の振る舞いは変えずに内部構造のみを変更し、
ソースコードを整理するテクニックである。
補足:リファクタリング時の注意
リファクタリングを行う際は
対象クラス・メソッドのユニットテストが存在していることが望ましい。
それは、リファクタリングによって振る舞いが変わってはいけないからである。
そこで、ユニットテストにより振る舞いを確認しながら内部構造を変更することで
開発者は安心してプロダクションコードを改善することができるようになる。
ここでは簡単なリファクタリング手法についての紹介のみにとどめるため、
より詳細なリファクタリングの手法については専門書を参照のこと。
JUnitにおけるリファクタリングの手法
概要
ここでは、処理の抽出によってモジュールの結合度を弱くして、
不確定要素の代わりに、確実な結果を返却するスタブやモックを差し替える
という方法を紹介する。
以下、下に行くにつれて結合度の弱い設計となっている。
結合度については"モジュール結合度について"という記事が参考になった。
- メソッドとして抽出
- クラスとして抽出
- インターフェースとして抽出
最適なパターンとは、一概に結合度が弱いものというわけではない。
リファクタリングには過剰設計という危険性もある。
適用した実装が、実現したい内容に対して大掛かりになりすぎるというもの。
実際にリファクタリングを行う際は、実現したいことと実装コストが
釣り合っているかを判断し、適切なリファクタリングを行うこと。
以下では、システム時間が設定されていることを確認するテストケースを例に
各種リファクタリング手法を紹介していく。
リファクタリング前のテストコード
public class Dateutils {
// (1)クラス初期化(=インスタンス化)の際にセット
Date date = new Date();
public void doSomethingToDate() {
// (2)メソッド実行時にセット
This.date = new date();
// 何らかの処理
}
}
public class DateUtilsTest {
@Test
public void doSomethingToDateでdateに現在時刻が設定される() throws Exception {
// (1)クラスのインスタンス化でdateにシステム時刻が設定される
DateUtils sut = new DateUtils();
// (2)メソッド内の処理でdateにシステム時刻が上書きされる
sut.doSomethingToDate();
// (3)isメソッドに渡されるシステム時刻はこの時点のシステム時刻
assertThat(sut.date, is(new Date()));
}
}
この実装の何がテスタビリティ(テストのしやすさ)を損ねているかというと、
実行結果が成功にも失敗にもなりうることである。
assertThatメソッドが想定するシステム時刻は(3)の時点で設定されるシステム時刻だが、
実測値として渡されているsut.date
は(2)のメソッドの実行時点でセットされた値である。
java.util.Dateクラスは日時をミリ秒まで保持するので、(2)〜(3)までの実行時間が
ミリ秒以上かかるとテストは失敗となる。
(上記実装ではまず発生しないが、doSomethingToDate内で1秒処理を停止させれば失敗する)
メソッドとして抽出
以下の実装では、システム時刻の取得をメソッドとして切り出し、
テストコードでオーバーライドによってスタブに差し替えている。
こうして、メソッドの実行タイミングという不確定な要素に依存していたテストケースを
リファクタリングし、テストコード側から確実な値をセットできるよう変更している。
厳密な話をすれば、システム時刻の取得もテスト対象メソッドである。
それをテストコード側から値を書き換えているという観点からすると、
これらは完璧なテストではない。しかし、不確定な要素を放置し、テスト対象クラス
というより大きな枠組みのテスト結果が安定しない方が問題なので、
テスタビリティの観点からは、テスト時の取り扱いが難しい処理は
個別のメソッドとして抽出し、テストコードから容易に操作できる設計が良い。
public class ExtractMethod {
Date date = new Date();
public void doSomethingToDate() {
// (1)により(2)メソッド実行時のシステム日付ではなくテスト実行時のシステム時刻がセットされる
this.date = getNowDate();
// 何らかの処理
}
Date getNowDate() {
// (2)本来のコードはメソッド実行時のシステム時刻をセットする
return new Date();
}
}
public class ExtractMethodTest {
@Test
public void doSomethingToDateでdateに現在時刻が設定される() throws Exception {
final Date current = new Date();
ExtractMethod sut = new ExtractMethod() {
// (1)setNowDateの処理を上書きし、currentのインスタンスをセットするよう変更
@Override
Date getNowDate() {
return current;
}
};
sut.doSomethingToDate();
assertThat(sut.date, is(sameInstance(current)));
}
}
クラスとして抽出
作りとしては、抽出、取得の対象がメソッドからクラスになっただけ。
クラスとして抽出することで、複数のテスト対象クラスから利用することができる。
ただし、テストの独立性の観点からあまり過剰な共通化は行わないこと。
(やりたいことに対してコーディング量が増えてきたので、過剰設計気味)
public class DateFactory {
Date getNowDate() {
return new Date();
}
}
public class ExtractClass {
DateFactory dateFactory = new DateFactory();
Date date = new Date();
public void doSomethingToDate() {
this.date = dateFactory.getNowDate();
// 何らかの処理
}
}
public class ExtractClassTest {
@Test
public void doSomethingToDateでdateに現在時刻が設定される() {
final Date current = new Date();
ExtractClass sut = new ExtractClass();
sut.dateFactory = new DateFactory() {
@Override
Date getNowDate() {
return current;
}
};
sut.doSomethingToDate();
assertThat(sut.date, is(sameInstance(current)));
}
}
インターフェースとして抽出
インターフェースとして抽出することで、複数クラスで共通して利用できるだけでなく、
getNowDateメソッドの中で行う処理も柔軟に変更できるようになった。
(下記の実装では完全に過剰設計)
public interface DateFactory {
Date getNowDate();
}
public class DateFactoryImpl implements DateFactory {
@Override
public Date getNowDate() {
return new Date();
}
}
public class ExtractClass {
DateFactory dateFactory = new DateFactoryImpl();
Date date = new Date();
public void doSomethingToDate() {
this.date = dateFactory.getNowDate();
// 何らかの処理
}
}
public class ExtractClassTest {
@Test
public void doSomethingToDateでdateに現在時刻が設定される() {
final Date current = new Date();
ExtractClass sut = new ExtractClass();
sut.dateFactory = new DateFactory() {
@Override
Date getNowDate() {
return current;
}
};
sut.doSomethingToDate();
assertThat(sut.date, is(sameInstance(current)));
}
}
参考文献
この記事は以下の情報を参考にして執筆しました。