【Spring Boot】@SpringBootTestのテストが重いときの代替策【Junit】

プロジェクト内の依存クラスが増加するにつれて、依存クラス全てを読み込むSpringBootTestは起動が重くなっていきます。
全てのテストクラスをSpringBootTestで実行すると、全体のテスト実行時間がかなり長くなってしまい開発に影響を及ぼします。
少しでもテスト実行時間を減らすために、全てSpringBootTestを使ってテストをするのではなくレイヤごとに必要な依存クラスのみを読み込ませることで時間を短くすることができます。

Controller層(@WebMvcTest)

MockMvc用でのController層に必要な依存クラスを読み込みます。

テスト対象

DemoApi.java

public interface DemoApi {

    @RequestMapping("/demoApi")
    ResponseEntity<DemoApiDto> get(DemoApiDto body);
}

修正前(@SpringBootTest)

@SpringBootTest
public class DemoApiSpringBootTest {

    private MockMvc mockMvc;

    @Autowired
    private DemoApi demoApi;

    @BeforeEach
    void setUp() {
        mockMvc = MockMvcBuilders.standaloneSetup(demoApi)
                .setControllerAdvice(new DemoHandler())
                .build();
    }

    @Test
    void isOK() throws Exception {
        // GIVEN
        DemoApiDto demoApiDto = DemoApiDto.builder()
                .demoName("hogeDemo")
                .demoObjectDto(
                        DemoObjectDto.builder()
                                .demoObjName("pugeObjName")
                                .build())
                .build();

        ObjectMapper objectMapper = new ObjectMapper();
        String request = objectMapper.writeValueAsString(demoApiDto);

        // WHEN
        // THEN
        this.mockMvc
                .perform(get("/demoApi")
                        .contentType(MediaType.APPLICATION_JSON_UTF8)
                        .content(request))
                .andDo(MockMvcResultHandlers.print())
                .andExpect(status().isOk());
    }
}

修正後(@WebMvcTest)

@WebMvcTest(controllers = DemoApiController.class)
public class DemoApiWebMvcTest {

    private MockMvc mockMvc;

    @Autowired
    private DemoApi demoApi;

    @BeforeEach
    void setUp() {
        mockMvc = MockMvcBuilders.standaloneSetup(demoApi)
                .setControllerAdvice(new DemoHandler())
                .build();
    }

    @Test
    void isOK() throws Exception {
        // GIVEN
        DemoApiDto demoApiDto = DemoApiDto.builder()
                .demoName("hogeDemo")
                .demoObjectDto(
                        DemoObjectDto.builder()
                                .demoObjName("pugeObjName")
                                .build())
                .build();

        ObjectMapper objectMapper = new ObjectMapper();
        String request = objectMapper.writeValueAsString(demoApiDto);

        // WHEN
        // THEN
        this.mockMvc
                .perform(get("/demoApi")
                        .contentType(MediaType.APPLICATION_JSON_UTF8)
                        .content(request))
                .andDo(MockMvcResultHandlers.print())
                .andExpect(status().isOk());
    }
}

Controllerがさらに依存クラスを持っている場合は、includeFiltersオプションを使います。
例えば、このControllerクラスに「HogeComponent」と「PugeService」が依存していた場合は以下のようにWebMvcTestを使います。

@WebMvcTest(controllers = DemoApiController.class,
    includeFilters = @ComponentScan.Filter(classes = {HogeComponent.class, PugeService.class}, type = FilterType.ASSIGNABLE_TYPE))

Service、Component層(@SpringJUnitConfig)

必要な依存クラスだけを読み込ませます。

テスト対象

DemoService.java(Componentを依存クラスとして持っている)

@Service
@RequiredArgsConstructor
class DemoService {

    private  final DemoComponent demoComponent;

    int triangleAreaTimesHeightByCalculation(int base, int height, int times){

        int timesHeight = demoComponent.timesCalculation(height,times);

        return base * timesHeight;
    }
}

修正前(@SpringBootTest)

@SpringBootTest
public class DemoServiceDISpringBootTest {

    @Autowired
    private DemoService target;

    @Test
    void triangleAreaTimesHeightByCalculationTest(){

        //GIVEN
        int base = 6;
        int height = 3;
        int times = 2;
        int excepted = 36;

        //WHEN
        int actual = target.triangleAreaTimesHeightByCalculation(base, height,times);

        //THEN
        assertThat(actual).isEqualTo(excepted);
    }
}

修正後(@SpringJUnitConfig)

@SpringJUnitConfig(classes = DemoServiceDISpringJunitConfigTest.Config.class)
public class DemoServiceDISpringJunitConfigTest {

    @Autowired
    private DemoService target;

    @ComponentScan({
            "com.example.demo.service",
            "com.example.demo.component"
    })
    static class Config {
    }

    @Test
    void triangleAreaTimesHeightByCalculationTest() {

        //GIVEN
        int base = 6;
        int height = 3;
        int times = 2;
        int excepted = 36;

        //WHEN
        int actual = target.triangleAreaTimesHeightByCalculation(base, height, times);

        //THEN
        assertThat(actual).isEqualTo(excepted);
    }
}

staticクラスを作り、依存するクラスをComponentScan対象にしていします。
そのクラスをアノテーションで指定することで、対象のクラスのみが読み込まれます。

Repository層(@MybatisTest)

Mybatisでデータベースアクセスを行っている場合に使えます。
データベースアクセスに必要な依存クラスを読み込みます。

テスト対象

AddressMapper.java

@Mapper
public interface AddressMapper {
    int create(Address address);

    @Results(id = "AddressByDistinct",
            value = {
                    @Result(column = "address_id", property = "addressId"),
                    @Result(column = "address", property = "address"),
                    @Result(column = "address2", property = "address2"),
                    @Result(column = "district", property = "district"),
                    @Result(column = "city_id", property = "city.cityId"),
                    @Result(column = "postal_code", property = "postalCode"),
                    @Result(column = "phone", property = "phone"),
                    @Result(column = "last_update", property = "lastUpdate")
            })
    @Select("SELECT * FROM address WHERE district = #{district}")
    List<Address> getAddressByDistrict(@Param("district") String distinct);
}

修正前(@SpringBootTest)

@SpringBootTest
class AddressMapperSpringBootTest {

    @Autowired
    private AddressMapper addressMapper;

    @Test
    void assertionTest() {

        //GIVEN
        String distinct = "Bihar";

        //WHEN
        List<Address> actual = addressMapper.getAddressByDistrict(distinct);

        //THEN
        //フィールドの値が全てNullではない
        assertThat(actual).extracting("address").allMatch(Objects::nonNull);
        //フィールドの値が全て空文字
        assertThat(actual).extracting("address2").allMatch(i -> "".equals(i));
        //フィールドの値が全てリストで指定した値
        assertThat(actual).extracting("city.cityId").allMatch(
                i -> List.of(264, 110, 346).contains(i));
    }
}

修正後(@MybatisTest)

@MybatisTest
class AddressMapperMybatisTest {

    @Autowired
    private AddressMapper addressMapper;

    @Test
    void assertionTest() {

        //GIVEN
        String distinct = "Bihar";

        //WHEN
        List<Address> actual = addressMapper.getAddressByDistrict(distinct);

        //THEN
        //フィールドの値が全てNullではない
        assertThat(actual).extracting("address").allMatch(Objects::nonNull);
        //フィールドの値が全て空文字
        assertThat(actual).extracting("address2").allMatch(i -> "".equals(i));
        //フィールドの値が全てリストで指定した値
        assertThat(actual).extracting("city.cityId").allMatch(
                i -> List.of(264, 110, 346).contains(i));
    }
}

あらかじめプロパティファイルなどで設定している接続先のDBをテストで使用する場合は、@AutoConfigureTestDatabaseアノテーションも使用します。

@MybatisTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class AddressMapperMybatisTest {

    @Autowired
    private AddressMapper addressMapper;

    @Test
    void assertionTest() {

        //GIVEN
        String distinct = "Bihar";

        //WHEN
        List<Address> actual = addressMapper.getAddressByDistrict(distinct);

        //THEN
        //フィールドの値が全てNullではない
        assertThat(actual).extracting("address").allMatch(Objects::nonNull);
        //フィールドの値が全て空文字
        assertThat(actual).extracting("address2").allMatch(i -> "".equals(i));
        //フィールドの値が全てリストで指定した値
        assertThat(actual).extracting("city.cityId").allMatch(
                i -> List.of(264, 110, 346).contains(i));
    }
}

springカテゴリの最新記事