mockito training - 53ningen/java-repo GitHub Wiki
dependency injection, mockitoなどの学習用コードとメモをまとめたモジュール
- 理想的なユニットテストは,依存するすべてのクラスや外部システムを使用したもの
- しかし依存する本物のオブジェクトを常に使用できるとは限らないし,ユニットテストで扱いにくいオブジェクトもある
- このようなときにテストしやすいようにリファクタリングしたり,本物のオブジェクトの代役となるオブジェクトに置き換えてテストを行うという手法が用いられる
- JavaのDateの精度はミリ秒であるため次のコードは,テストに成功したり失敗したりする
- このようにインスタンス生成のタイミングに依存してしまう部分が含まれるコードのテストは難しい
public class DateDependencyExample {
Date date = new Date();
public void doSomething() {
this.date = new Date();
// do something...
}
}
public class DateDependencyExampleTest {
@Test
public void doSomethingでdateに現在時刻が設定される() throws Exception {
DateDependencyExample dateDependency = new DateDependencyExample();
dateDependency.doSomething();
// このassertionは実行タイミングによって成功したり失敗したりする
assertThat(dateDependency.date, is(new Date()));
}
}- インスタンス生成の部分を抽出してファクトリメソッドを作ると,ここをOverrideして振る舞いを変えることができ,依存度を下げることができる
public class DateDependencyExample {
Date date = newDate();
public void doSomething() {
this.date = newDate();
// do something...
}
}
Date newDate() {
return new Date();
}
public class DateDependencyExampleTest {
@Test
public void doSomethingでdateに現在時刻が設定される() throws Exception {
final Date currentDate = new Date();
DateDependencyExample dateDependency = new DateDependencyExample() {
@Override
Date newDate() {
return current;
}
};
dateDependency.doSomething();
// このassertionは実行タイミングによって成功したり失敗したりする
assertThat(dateDependency.date, is(current));
}
}- 今回の例に対してはやや過剰な実装とはなるが,振る舞いの複雑なメソッドをオブジェクトに切り出し,委譲を使って差し替え可能にするという方法もある
public class DateFactory {
Date newDate() {
return new Date();
}
}
public class DateDependencyExample {
DateFactory dateFactory = new DateFactory();
Date date = new Date();
public void doSomething() {
date = dateFacotory.newDate();
// do something...
}
}
public class DateDependencyExampleTest {
@Test
public void doSomethingを実行するとdateに現在時刻が設定される() throws Exception {
final Date current = new Date();
DateDependencyExample dateDependency = new DateDependencyExample();
// dateインスタンス生成の振る舞いを注入している
dateDependency.dateFacotry = new Date() {
@Override
public Date newDate() {
return current;
}
};
dateDependency.doSomething();
assertThat(dateDependency.date, is(sameInstance(current)));
}- テスト対象となるクラスが他クラスに依存していないケースはほとんどない
- 依存しているクラスもまた他のクラスに依存している
- この状態でテストを行うメリットは実行時に近い状態でテストができること
- デメリットはテスト対象以外の問題を原因としてテストが失敗する可能性があること
- このとき依存しているオブジェクトの代役(スタブ・モック)を使うことを
テストダブルという
- スタブとは依存するクラスやモジュールとして代用できる仮のクラス・モジュール
- 以下のようなときに用いられる
- 依存オブジェクトが予想できない振る舞いを持つとき
- 依存オブジェクトのクラスがまだ存在しないとき
- 依存オブジェクトの実行コストが高く,簡単に利用できない
- 依存オブジェクトが実行環境に強く依存している
- 例えば乱数を用いるようなクラスのテストは,依存オブジェクトRandomが予想できない振る舞いを持つためスタブを使うと良い
- コード的には次のようなものになる
// RandomNumberGenerator.java
package com.ningen.gomi.testdouble;
public interface RandomNumberGenerator {
int nextInt();
}
// RandomNumberGeneratorImpl.java
package com.ningen.gomi.testdouble;
import java.util.Random;
public class RandomNumberGeneratorImpl implements RandomNumberGenerator {
private final Random rand = new Random();
@Override
public int nextInt() {
return rand.nextInt();
}
}
// RandomNumberGeneratorFixedResultStab.java
package com.ningen.gomi.testdouble;
public class RandomNumbergeneratorFixedResultStub implements RandomNumberGenerator {
@Override
public int nextInt() {
return 1;
}
}
// Randoms.java
package com.ningen.gomi.testdouble;
import java.util.List;
public class Randoms {
RandomNumberGenerator randomNumberGenerator = new RandomNumberGeneratorImpl();
public <T> T choice(List<T> options) {
if(options.size() == 0) return null;
int index = randomNumberGenerator.nextInt() % options.size();
return options.get(index);
}
}
// RandomsTest.java
package com.ningen.gomi.testdouble;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
public class RandomsTest {
@Test
public void choiceをAで返すスタブを用いたテスト() throws Exception {
List<String> options = new ArrayList<String>();
options.add("a");
options.add("b");
Randoms sut = new Randoms();
// inject stab
sut.randomNumberGenerator = new RandomNumbergeneratorFixedResultStub();
assertThat(sut.choice(options), is("b"));
}
@Test
public void choiceをAで返す匿名クラスでスタブを注入したテスト() throws Exception {
List<String> options = new ArrayList<String>();
options.add("a");
options.add("b");
Randoms sut = new Randoms();
// inject stab
sut.randomNumberGenerator = new RandomNumberGeneratorImpl() {
@Override
public int nextInt() {
return 0;
}
};
assertThat(sut.choice(options), is("a"));
}
}