๐Ÿ“ Spring Boot Test Cheat Sheet ์ž…๋‹ˆ๋‹ค. ์„ค๋ช…์€ ์—†๊ณ  ์ž์ฃผ ์‚ฌ์šฉํ•˜๋Š” ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ฐธ๊ณ ์šฉ ์ž…๋‹ˆ๋‹ค.


build.gradle

dependencies {
    runtimeOnly 'com.h2database:h2'
    implementation 'com.google.guava:guava:31.1-jre'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'io.rest-assured:rest-assured'
}



application-test.yml

spring:
  datasource:
    url: jdbc:h2:mem:test
    username: sa
    password:
    driver-class-name: org.h2.Driver
  jpa:
    hibernate:
      ddl-auto: 'create-drop'



import

import static org.hamcrest.Matchers;
import static org.junit.jupiter.api.Assertions.*;
import static org.assertj.core.api.Assertions.*;



DatabaseCleanup

JPA์šฉ, ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋‚ด ๋ชจ๋“  ์—”ํ‹ฐํ‹ฐ ํ…Œ์ด๋ธ” ์ดˆ๊ธฐํ™” ํด๋ž˜์Šค

@Service
@ActiveProfiles("test")
public class DatabaseCleanup implements InitializingBean {
    @PersistenceContext
    private EntityManager entityManager;

    private List<String> tableNames;

    @Override
    public void afterPropertiesSet() {
        tableNames = entityManager.getMetamodel().getEntities().stream()
                .filter(e -> e.getJavaType().getAnnotation(Entity.class) != null)
                .map(e -> CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, e.getName()))
                .collect(Collectors.toList());
    }

    @Transactional
    public void execute() {
        entityManager.flush();
        entityManager.createNativeQuery("SET REFERENTIAL_INTEGRITY FALSE").executeUpdate();

        for (String tableName : tableNames) {
            entityManager.createNativeQuery("TRUNCATE TABLE " + tableName).executeUpdate();
            entityManager.createNativeQuery("ALTER TABLE " + tableName + " ALTER COLUMN ID RESTART WITH 1").executeUpdate();
        }

        entityManager.createNativeQuery("SET REFERENTIAL_INTEGRITY TRUE").executeUpdate();
    }
}



Acceptance Abstract

RestAssured ์‚ฌ์šฉ์‹œ ๊ณตํ†ต ๋ถ€๋ชจ ํด๋ž˜์Šค. ๋žœ๋คํฌํŠธ์™€ ํ…Œ์ด๋ธ” ์ดˆ๊ธฐํ™”

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public abstract class AcceptanceTest {

    @LocalServerPort
    int port;

    @Autowired
    private DatabaseCleanup databaseCleanup;

    @BeforeEach
    public void setUp() {
        RestAssured.port = port;
        databaseCleanup.execute();
    }
}



RestAssured

public static ExtractableResponse<Response> ์ž๋™์ฐจ_๋“ฑ๋ก_์š”์ฒญ(String name) {
    Map<String, Object> params = new HashMap<>();
    params.put("name", name);

    return RestAssured
            .given().log().all()
            .body(params)
            .contentType(MediaType.APPLICATION_JSON_VALUE)
            .when().post("/api/cars")
            .then().log().all()
            .extract();
}
public static void ์ž๋™์ฐจ_๋“ฑ๋ก๋จ(ExtractableResponse<Response> response) {
    assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value());
    assertThat(response.header("Location")).isNotBlank();
}
public static ExtractableResponse<Response> ์ •๋ณด_์กฐํšŒ_์š”์ฒญ(String accessToken) {
    return RestAssured
            .given().log().all()
            .auth().oauth2(accessToken)
            .when().get("/members/me")
            .then().log().all()
            .extract();
}



@ExtendWith(SpringExtension.class), @MockBean

@ExtendWith(SpringExtension.class)
public class SpringExtensionTest {

    @MockBean
    private LineRepository lineRepository;

    @MockBean
    private StationService stationService;

    @Test
    void findAllLines() {
        // given
        when(lineRepository.findAll()).thenReturn(Lists.newArrayList(new Line()));
        LineService lineService = new LineService(lineRepository, stationService);

        // when
        List<LineResponse> responses = lineService.findLineResponses();

        // then
        assertThat(responses).hasSize(1);
    }
}



@ExtendWith(MockitoExtension.class), @Mock, @InjectMocks

@ExtendWith(MockitoExtension.class)
public class MockitoExtensionTest {

    @Mock
    private CarRepository carRepository;

    @InjectMocks
    private CarService carService;

    @DisplayName("์ด๋ฆ„์„ ๋ฐ›์•„ ์ž๋™์ฐจ๋ฅผ ์ €์žฅํ•œ๋‹ค.")
    @Test
    public void save(){
        // given
        final String NAME = "Boong-Boong";
        when(carRepository.save(any())).thenReturn(Optional.of(new Car(NAME)));
        
        // when
        CarResponse carResponse = carService.save(NAME);

        // then
        assertThat(carResponse.getName()).isEqualTo(NAME);
    }
}



@WebMvcTest, @MockBean, MockMvc

@WebMvcTest(CarController.class)
public class CarControllerTest {

    private static final String API_BASE_PATH = "/api/cars";

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private CarService carService;

    @Autowired
    private ObjectMapper objectMapper;

    @DisplayName("์ž๋™์ฐจ๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.")
    @Test
    public void createCar() throws Exception {
        //given
        CarRequest carRequest = new carRequest("Boong-Boong");
        CarResponse carResponse = new CarResponse(1L, carRequest.getName());
        given(carService.save(any())).willReturn(carResponse);

        //when, then
        mockMvc.perform(post(API_BASE_PATH)
                .content(objectMapper.writeValueAsString(carRequest))
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.id").value(carResponse.getId()));
    }
}



@SpringBootTest, @AutoConfigureMockMvc, MockMvc

@SpringBootTest
@AutoConfigureMockMvc
public class CarControllerMockTest {

    private static final String API_BASE_PATH = "/api/cars";

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @DisplayName("์ž๋™์ฐจ๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.")
    @Test
    public void createCar() throws Exception {
        //given
        CarRequest carRequest = new carRequest("Boong-Boong");
        CarResponse carResponse = new CarResponse(1L, carRequest.getName());

        //when, then
        mockMvc.perform(post(API_BASE_PATH)
                .content(objectMapper.writeValueAsString(carRequest))
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.id").value(carResponse.getId()));
    }
}



@DataJpaTest, @PersistenceContext

  • JPA ๊ด€๋ จ ์„ค์ •๋งŒ ๋กœ๋“œ
  • ๋ฐ์ดํ„ฐ ์†Œ์Šค, ์—”ํ‹ฐํ‹ฐ ๋งค๋‹ˆ์ € ๋“ฑ ์ƒ์„ฑ
  • @Entity ์–ด๋…ธํ…Œ์ด์…˜์ด ๋ถ™์€ ํด๋ž˜์Šค ๋ฐ Spring Data JPA ์— ๋Œ€ํ•œ ์„ค์ •๋“ค ์ ์šฉ
  • ๊ธฐ๋ณธ์œผ๋กœ in-memory database ์‚ฌ์šฉ
  • @AutoConfigureTestDatabase๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์–ด๋–ค database๋ฅผ ์—ฐ๊ฒฐํ• ์ง€ ์„ ํƒํ•  ์ˆ˜ ์žˆ๋‹ค.
@DataJpaTest
@ActiveProfiles("test")
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class CarRepositoryTest {

    @Autowired
    private CarRepository carRepository;

    @PersistenceContext
    private EntityManager entityManager;

    @Test
    @DisplayName("์ž๋™์ฐจ๋ฅผ ์ €์žฅํ•œ๋‹ค.")
    public void save() {
        
        ...

        Car savedCar = carRepository.save(new Car("Boong-Boong"));

        entityManager.flush();
        entityManager.clear();
        
        ...
    }
}



@MybatisTest

  • Mybatis ํ…Œ์ŠคํŠธ ๊ฐ€์ด๋“œ
  • ๊ธฐ๋ณธ์œผ๋กœ in-memory database ์‚ฌ์šฉ
  • @AutoConfigureTestDatabase๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์–ด๋–ค database๋ฅผ ์—ฐ๊ฒฐํ• ์ง€ ์„ ํƒํ•  ์ˆ˜ ์žˆ๋‹ค.
@ExtendWith(SpringExtension.class)
@MybatisTest
@ActiveProfiles("test")
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class OrderTest {

    @Autowired
    private OrderMapper orderMapper;

    @Test
    public void mybatis_test() throws Exception {

        // given
        String seq = "1";

        // when
        OrderVO vo = orderMapper.getOrder(seq);

        // then
        assertThat(vo.getSeq).isEqualTo("1");

    }
}



@Mock, @MockBean ์ฐจ์ด์ 

Mock ์ข…๋ฅ˜ ์˜์กด์„ฑ ์ฃผ์ž… ๋Œ€์ƒ
@Mock @InjectMocks
@MockBean Spring Context
  • @Mock์€ @InjectMocks์— ๋Œ€ํ•ด์„œ๋งŒ ํ•ด๋‹น ํด๋ž˜์Šค์•ˆ์—์„œ ์ •์˜๋œ ๊ฐ์ฒด๋ฅผ ์ฐพ์•„์„œ ์˜์กด์„ฑ์„ ํ•ด๊ฒฐ
  • @MockBean์€ mock ๊ฐ์ฒด๋ฅผ ์Šคํ”„๋ง ์ปจํ…์ŠคํŠธ์— ๋“ฑ๋ก


MockMVC ์ฃผ์ž…๋ฐฉ๋ฒ•

  1. @SpringBootTest + @AutoConfigureMockMvc : MockMVC๋ฅผ ์ด์šฉํ•˜์—ฌ ํ†ตํ•ฉํ…Œ์ŠคํŠธ ํ•  ๋•Œ ์‚ฌ์šฉ
  2. @WebMvcTest : MVC๋งŒ ์Šฌ๋ผ์ด์Šค(slice) ํ…Œ์ŠคํŠธ ํ•  ๋•Œ ์‚ฌ์šฉ

๋Œ“๊ธ€๋‚จ๊ธฐ๊ธฐ