はじめに
単体試験をがっつりJunitで行う現場に初めて入り、
今まで「Junit」という名前しか聞いたことのなかった私が
日々アワアワしながらテストコードを書いていた時に知ったJunitに関する備忘録+陥ったことの紹介です。
なので初心者向けだと思いますが、Junitって何?とか設定とか基本的な書き方とかそこらへんは調べたら山のように出てくると思うのであまり丁寧に書きません。
環境と依存関係
Junit : Junit5
IDE : Eclipse
Java : 8
ユニットテストでモックするフレームワークとしてMockitoを使用しており
Mavenでプロジェクト作成していたのでpom.xmlに依存関係追記しています。
(ここは環境に合わせてご自由に)
<dependencies>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>1.10.19</version>
<scope>test</scope>
</dependency>
</dependencies>
試験対象のメソッドの中で、未完成の他クラスのメソッドを呼び出している
テスト対象コード
サンプルコード。試験対象はSampleAクラスのaMethodですが、その中で呼ばれているSampleBクラスが未完成と仮定します。
public class SampleA {
public boolean aMethod(String hoge) {
SampleB sampleB = new SampleB();
return sampleB.bMethod(hoge);
}
}
// 未完成
public class SampleB {
public boolean bMethod(String hoge) {
return false;
}
}
テストコード
public class SampleTest {
@Test
public void test() {
try(
MockedConstruction<SampleB> bMock = mockConstruction(SampleB.class,
(mock, context) ->
Mockito.doReturn(true).when(mock).bMethod(any(String.class)))
) {
SampleA sut = new SampleA();
boolean result = sut.aMethod(hoge);
//assertEqualsで戻り値の確認もするけどここは省略
}
}
}
SampleBクラスをモック化し、MockitoのdoReturnでbMethodが呼ばれたときにはtrueが返却されるよう定義します。
(ちなみに)私が書いたNGパターン
public class SampleTest {
@Test
public void test() {
// SampleB モック化
SampleB bMock = mock(SampleB.class);
// bMethodの挙動をスタブ化(試験用にtrueを返すよう設定)
when(bMock.bMethod(ArgumentMatchers.any(String.class))).thenReturn(true);
SampleA sut = new SampleA();
boolean result = sut.aMethod(hoge);
//assertEqualsで戻り値の確認もするけどここは省略
}
}
Bクラスをモック化して、試験用にtrueが返却されるようスタブも設定して、
いざ試験すると返却されるのはFalse・・・。
なぜ・・・?
今回のパターンではAクラスの中で生成されているBクラスインスタンスに対してモックを作成したいんです。
aMethodをboolean result = sut.aMethod(hoge);
で実行していますがこの中ではBクラスの実インスタンスが生成されてしまいます。(上記の例だとまだ作成途中のためfalseしか返りません)
(たしかにデバッグして確認してたんですが、モック作成されていないしまだ未完成のメソッド通ってやがる、と思ってました。)
この書き方では、試験対象のメソッドの中で他クラスのインスタンスをnewしている場合ではモックは作成されません。
ここで「MockedConstruction」
try(
MockedConstruction<SampleB> bMock = mockConstruction(SampleB.class,
(mock, context) ->
Mockito.doReturn(true).when(mock).bMethod(any(String.class)))
)
MockedConstructionメソッドを使用して対象のクラスのモックを作成します。
ここで指定したクラスのインスタンスが作成された時にはモックされた状態にしてくれます。
(少し厳密に言うと、mockConstruction() が返却するのは、指定したクラスのモックを制御するためのインスタンスで、mock自体ではない、らしい)
その後に、mockConstruction() の第2引数に試験的な振る舞いを定義します。上の例では、「メソッドが呼ばれたらtrueを返却してね」と定義しています。
これでsampleB.bMethod(hoge)
が呼ばれたらtrueが返却されるようになります。
ちなみにtryで囲うとMockConstructionで作成したインスタンスを勝手にcloseしてくれます。close()を呼ばなくて良いので便利です。同テストクラスの他メソッドもいっぺんに実施したい時にも競合せず、各メソッドの中で個々のモックインスタンスを使用できます。
その他JuntiTips集
試験対象クラスの一部だけ試験したい、一部のメソッドは振る舞いを変えたいパターン
import static org.mockito.BDDMockito.willReturn;
@test
public void testSpy() {
SampleClass sampleSpied = spy(new SampleClass());
// 試験対象外メソッドの挙動を定義
willReturn("hoge").given(sampleSpied).stubbingMethod("hoge");
}
試験対象クラスをスパイし、willReturn,givenで挙動を定義します。
BDDMockito.willReturnのimportを忘れずに
(みんなの敵)privateメソッドのテスト
対象試験の中でprivateメソッドを実行する方法です。
が、privateメソッドの試験は直接行うべきではなく、呼び出し元のメソッド経由で行うべきなのでそもそもprivateメソッドの試験をいくつか行うようであれば設計見直しをした方がよいらしい。。
privateメソッドそのものの振る舞いを定義することはJunit5では出来ないので、今回はリフレクションによってアクセスする形でユニットテストを書きます。
テスト対象コード
public PrivateSample {
private String privateMethod(String hoge) {
return "retrun some value";
}
}
テストコード
@test
public void testPrivateMethod {
PrivateSample sut = new PrivateSample();
Method method = PrivateSample.class.getDeclaredMethod("privateMethod", String.class);
// 対象privateメソッドに対して外部からアクセスすることを許可する
method.setAccessible(true);
// 抽出したprivateMethodメソッドを実行
method.invoke(sut, "hoge");
}
おわりに
今回ここで書いたのは、Junitでテストコードを書いている時に自分で調べてもよく分からなかった点ついてフォーカスしてまとめました。
他にも色々と「メソッド呼び出し回数によって挙動を変える」とか「Exceptionを発生させる試験」とか色々なパターンでJunitテストコードを書く機会がありましたが、そこらへんは調べたらすぐ出てくるので省いています。
また書く機会があればスラスラと書けるようになってたらいいな・・・。