JUnitでは、アサーションという検証を行うためのクラスを使用する。
しかし、このアサーションは以下のように、たくさんのパッケージがあり、どれを使用(import)するかで、メソッドの使い方が変わるので注意する。
- org.assertj.core.api.Assertions
- org.junit.jupiter.api.Assertions
- org.junit.Assert
org.assertj.core.api.Assertions ががシンプルに使えておすすめ。
基本形
@ExtendWith(SpringExtension.class)
class SampleTest {
// 検証するサービスのMock
@InjectMocks
static private SampleService service;
// 引数を検証する場合
@Captor
private ArgumentCaptor argCaptor;
@BeforeEach
void setUp() throws Exception {
// 共通事前処理
}
@AfterEach
void tearDown() throws Exception {
// 共通事後処理
reset (service);
}
@DisplayName("setNewCookie")
@Test
void sampleTest() {
Targ "sample";
// テスト実施
T actualResponse = service.sampleMethod(arg); // 検証するメソッドを実行
// sampleMethod()内の特定のメソッドがコールされたか
verify(service, times(1)).createInitRequest(argCaptor.capture()); // 渡された引数を検証する場合はCaptorを使用
// 値の検証
assertThat(argCaptor.getValue()).isEqualTo("expectedArg");
assertThat(actualResponse.getParamname()).isEqualTo("expectedResponse");
// 値の検証(参考)
assertThat(expectResponse).isEqualTo(actualResponse); // オブジェクトが同一
assertSame (expectResponse, actualResponse); // 参照が同一
assertNull(actualResponse); // Null
assertNotNull(actualResponse); // Nullでない
assertTrue(actualResponse > 0); // True
}
}
引数の検証
テスト実行時、メソッドに渡された引数が何であったかを確認する。
@Captor
private ArgumentCaptor argCaptor; // Tには引数の型を指定
@Test
void methodTest() {
service.methodName(argCaptor.capture()); // この時点で渡された引数を捕捉
assertThat(argCaptor.getValue()).isEqualTo("expectedVal");
}
例外が送出されたかのテスト
以下ではNamingException が出力されるケースの検証。
ただし、クラス自体が例外をスローしている場合のみ。検証メソッド内で、例外をcatch処理している場合は、catch内の処理が行われたか?を別途検証する内容でテストを作成する必要がある。
assertThrows (NamingException.class, () -> {
service.methodName();
});
もしくはorg.assertj.core.api.Assertions.assert That ThrownByを使用して、
assertThatThrownBy(() -> service.methodName()).isInstanceOf(Exception.class);
例外のBody部の検証
RestTemplateを使用した外部通信のレスポンスに、401 UNAUTHORIZEDの例外が返却されるとき、例外のBody部を検証する方法。
@Test
public void testHttpClientErrorException() throws IOException {
// モックされたClientHttpResponseの作成
ClientHttpResponse mockResponse = mock(ClientHttpResponse.class);
// レスポンスボディの設定
String responseBody = "{\"specificParam\":\"testValue\"}";
// HttpClientErrorException をスローするRestTemplateのモックを作成
RestTemplate restTemplate = mock(RestTemplate.class); // restTemplateのMock方法は任意
doThrow(new HttpClientErrorException(HttpStatus. UNAUTHORIZED, HttpStatus.UNAUTHORIZED.getReasonPhrase(),
new HttpHeaders(), responseBody.getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8))
.when(restTemplate).getForEntity (Mockito. anyString(), Mockito.eq(String.class)); // getForEntityは検証メソッドのリクエスト形式に応じて適宜修正
// テスト対象メソッドの呼び出し
Exception exception = assertThrows(HttpClientErrorException.class, () -> {
service.sampleMethod();
});
// 例外のボディを検証
HttpClientErrorException clientErrorException = (HttpClientErrorException) exception;
String body = clientErrorException.getResponseBodyAsString();
assertEquals(responseBody, body);
// JSONの特定のパラメータを検証
ObjectMapper objectMapper = new ObjectMapper();
JsonNode rootNode = objectMapper.readTree (body);
String specificParam = rootNode.path("specificParam").asText();
assertEquals("testValue", specificParam);
}
テストのグループ化
@Nestedを使用してクラスをネストすることで、テストをグループ化できる。
ただし、インスタンスが別になるため、BeforeEachなどで定義したフィールドのうち、staticで定義していないものは再定義する必要がある。
@ExtendWith(SpringExtension.class)
classSampleTest{
@DisplayName("sampleGroup")
@Nested
public class sampleGroup {
@DisplayName("sampleGroup")
@Test
void sampleTest() {
}
}
}
staticのすすめ
@Nestedでグループ化する場合、このアノテーションでくくった単位でインスタンス化される。
staticなフィールドとして定義しておけば、クラス全体で共有されるため、複数のインスタンスが作成されてもアクセスできる。
staticはクラスメソッドとしてアクセスできるため、スコープがオープンになる。また、マルチスレッドのためデータの競合が起きる可能性がある。
これらの条件から扱いが難しいが、JUnitのテストにおいてはシングルスレッドで、スコープも意識しないため、できるかぎりstaticを使用する癖をつけると良い。
@AfterAllでreset(service)を使用して、初期化処理をいれておく点に注意する。
privateメソッドのテスト
プライベートメソッドは、スコーブがプライベートなため、テストクラスから参照できない。
java.lang.reflect.Methodを使用して、スコープをアクセス可能にする。
Method method = this.callServiceImpl.getClass().getDeclaredMethod("メソッド名", T[引数の型], T[引数2の型], …);
@DisplayName("test")
@Test
voidtest() {
Method method = this.service.getClass().getDeclaredMethod("setNewCookie", String.class)
method.setAccessible(true); //privateメソッドでもアクセスできるようにする
method.invoke(this.service,"testArg");
}
戻り値を受け取る必要がある場合、method.invoke()の戻り値はObject型になるため、キャストする必要がある点に注意する。
ResponseType response = (ResponseType) method.invoke(this.service,"testArg");
上記は標準のJava機能で実装できるが、PowerMockを使うと以下。
String methodName = "methodName";
Method method = PowerMockito.method(serviceImpl.class, methodName, Arg.class);
// メソッド実行
Treturn = (T) method.invoke(cnt1CommonService, arg);
getDeclaredMethod()でエラー
@ExtendWithや、@InjectMocksがうまく設定できていないとエラーになるので、まずは基本形を確認。
関数名、引数の型と数が間違っている可能性を確認。
method.invokeでエラー
エラーメッセージ
java.lang.IllegalArgumentException: object is not an instance of declaring class
invokeするときの第一引数に、テストクラス自身を指定する必要がある。
method.invoke("testArg"); //NG
method.invoke(this.service,"testArg"); //OK
外部APIとの通信
そのクラス内でテストできない部分の通信や、すでに動作が保証されている通信部分は、Mockを使用して、返ってくるレスポンスを事前に設定する。
Response responseMock = new Response();
Mockito.when(service.sampleMethod(any())) //apiInvokeServiceOsearch
.thenReturn(response); //responseMockを返却する
サーブレットのMock
SpringFrameworkのMockHttpServletResponseを使用する。
MockHttpServletResponse httpServletResponse = new MockHttpServletResponse();
MockItoだと値を追跡できないが、これだとできる。
Appendix
クライテリアの検証
ORマッパーにMyBatisを指定している場合、DBとの通信に使用するクエリはExampleとを用いて指定する。
この条件(Criteria)を検証したい場合は、以下のようにCriteriaをキャプチャする。
ArgumentCaptor capHoge Example = ArgumentCaptor.forClass(hogeExampl.class);
assertThat (capExample.getValue().getOredCriteria().get(0).getCriteria().get(0).getCondition()).isEqualTo(expectedCondition); // クライテリアに設定された条件を検証
assertThat (capExample.getValue().getOredCriteria().get(0).getCriteria().get(0).getValue()).isEqualTo(expectedValue); // クライテリアに設定された値を検証
※クライテリアに設定された条件が2つ以上の場合は.get(0)以外の複数のクライテリアを検証する
インシデント
assertThatでエラー
突然今まで使えていたassertThatでエラーが出るようになった。
■実装
assertThat(str1).isEqualTo(str2);
■エラーメッセージ
型 Assert のメソッド assertThat(String T, Matcher<? super T>) は引数 (String) に適用できません
eclipseの補完機能でimport文を生成している場合に、意図せずパッケージが変わってしまうことがある。
これも、importするパッケージが異なるためのエラー。org.junit.Assertではシンプルな比較ができない。以下のパッケージをimportする。
import static org.assertj.core.api.Assertions.*:
ResponseEntityのHttpHeaderに値を設定するとエラー
以下のような、ResponseEntityのヘッダーを加工するメソッドを、JUnitで検証したい。
■Sample.java
private void sampleMethod(ResponseEntity responseEntity) {
HttpHeaders httpHeaders = responseEntity.getHeaders();
httpHeaders.add("sampleKey", "sampleVal");
}
その場合、以下のようなテストコードを記述することになる。
■SampleTest.java
@Test
private void sampleMethodTest() {
HttpHeaders httpHeaders = new HttpHeaders();
String sampleBody = "sampleVal";
ResponseEntity<Object> responseEntity = new ResponseEntity<>(sampleBody, httpHeaders, HttpStatus.ACCEPTED);
}
これを実行すると、httpHeaders.add()の部分で、UnsupportedOperationExceptionが発生する。
java.lang.UnsupportedOperationException
at org.springframework.http.ReadOnlyHttpHeaders.set(ReadOnlyHttpHeaders.java:106)
at org.springframework.http.HttpHeaders.setBasicAuth(HttpHeaders.java:816)
ResponseEntityはイミュータブルであり、作成した時点で、登録したHttpHeadersは、ReadOnlyHttpHeadersに書き換えられる。
このヘッダーにプロパティを追加したり、改変しようとするとエラーが発生する。
コメント