Spring Boot Test Cheat Sheet
๐ 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 ์ฃผ์ ๋ฐฉ๋ฒ
- @SpringBootTest+- @AutoConfigureMockMvc: MockMVC๋ฅผ ์ด์ฉํ์ฌ ํตํฉํ ์คํธ ํ ๋ ์ฌ์ฉ
- @WebMvcTest: MVC๋ง ์ฌ๋ผ์ด์ค(slice) ํ ์คํธ ํ ๋ ์ฌ์ฉ
๋๊ธ๋จ๊ธฐ๊ธฐ