【Java】JUnitテスト基礎

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に書き換えられる。

このヘッダーにプロパティを追加したり、改変しようとするとエラーが発生する。

コメント

タイトルとURLをコピーしました