そこに仁義はあるのか(仮)

略してそこ仁!

他のクラスへの「呼び出し回数」と「引数として渡した値」をテストする

久しぶりにテストコードを書いたのでMEMO〜。

アプリから他のサービスを使っているけど、自動テストの時はそのサービスを使いたくない。
もしくは、テスト対象のクラスから他のクラスのメソッドを呼び出しているが、他のクラスの仕様はそのクラスのテストに委譲しており、今回のテストには含めたくない。
ただ、作成したクラスからサービスや他のクラスに対して、どんな値が渡されたのかはチェックしたい。

f:id:syobochim:20190630163339p:plain:w500

例えば、Controllerクラスのテストをする場合、 Controller内で Fukuzatsu クラスの hikisuIppai()メソッドを呼び出しているとする。
でも、hikisuIppai()メソッドの挙動は Fukuzatsu クラス側でテストするから、hikisuIppai() メソッドの「呼び出し回数」「引数として渡された値」が想定通りに設定されていることだけをテストできればOKとする。
そうした時のテストクラスはこんな感じになる。

import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.test.web.servlet.MockMvc;

import java.math.BigDecimal;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import static org.hamcrest.core.Is.is;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;

@AutoConfigureMockMvc
@SpringBootTest
class ControllerTest {
  @Autowired
  private MockMvc mockMvc;
  @SpyBean
  private Fukuzatsu fukuzatsu;
  @Captor
  private ArgumentCaptor<Hotel> hotel;
  @Captor
  private ArgumentCaptor<Reservation> reservation;

  @Test
  void detailTest(@Autowired ObjectMapper mapper) throws Exception {
    doReturn(null).when(this.fukuzatsu).hikisuIppai(any(), any(), any(), any());

    CreateCommand cmd = new CreateCommand(hoge, fuga, piyo);
    String json = mapper.writeValueAsString(cmd);
    mockMvc.perform(post("/detail").content(json));

    verify(this.fukuzatsu, times(1)).hikisuIppai(any(), hotel.capture(), reservation.capture(), any());
    assertThat(reservation.getValue().getRating(), allOf(greaterThanOrEqualTo(new BigDecimal(0)), lessThanOrEqualTo(new BigDecimal(5))));
    assertThat(reservation.getValue().getNumberOfPeople(), is(greaterThanOrEqualTo(0)));
  }
}

内部の処理をスキップさせるために@SpyBean を利用して Fukuzatsu クラスの hikisuIppai メソッドの処理をモック化している。
今回は、引数に何が来ても null を返すように。

  @SpyBean
  private Fukuzatsu fukuzatsu;

...

    doReturn(null).when(this.fukuzatsu).hikisuIppai(any(), any(), any(), any());

アサーションでは、 hikisuIppai メソッドの呼び出し回数と、引数として渡した値を検査している。
キャプチャできれば普通に is メソッドなどを使って比較ができる。 今回は、

  • hikisuIppaiメソッドの呼び出し回数が1回であること
  • hikisuIppai メソッドの2番目、3番目に渡した値が想定通りであること
    • 2番目の値は hotel プロパティにキャプチャして、 hotel.getValue() で値を取得している。
    • 3番目の値は reservation プロパティにキャプチャして、 reservation.getValue() で値を取得している。

を検査している。

  @Captor
  private ArgumentCaptor<Hotel> hotel;
  @Captor
  private ArgumentCaptor<Reservation> reservation;

...

    verify(this.fukuzatsu, times(1)).hikisuIppai(any(), hotel.capture(), reservation.capture(), any());
    assertThat(hotel.getValue().getRating(), allOf(greaterThanOrEqualTo(new BigDecimal(0)), lessThanOrEqualTo(new BigDecimal(5))));
    assertThat(reservation.getValue().getNumberOfPeople(), is(greaterThanOrEqualTo(0)));