[REST API] 19. Blog V3 : Integration Test

문정준's avatar
May 15, 2025
[REST API] 19. Blog V3 : Integration Test
💡

Integration Test (통합 테스트)

  1. 로컬에서 테스트 하는 환경이 실제 환경에서도 적용되는 지 확인하기 위함
  1. 코드를 수정하였을 때에 다른 코드에 영향이 있는지 확인하기 위함
 
 

💡 통합 테스트 vs 화이트박스 테스트 (WhiteBox Testing)

  • 통합 테스트는 개별 모듈이 서로 잘 연동되어 전체 시스템이 제대로 작동하는 지 확인하는 테스트
  • 화이트박스 테스트는 시스템 내부 코드 구조를 알고 테스트를 진행하는 방식
    • 통합 테스트를 진행하는 방향 중 하나로 화이트박스 테스트를 진행할 수 있음
    • 통합 테스트 ≠ 화이트박스 테스트 ⇒ 통합 테스트 ⊂ 화이트박스 테스트
 
  • UserControllerTest
    • MockMvc, ObjectMapper 사용 : JSON 변환 후 fetch
    • Test의 Transactional : 테스트가 다 끝난 후 Rollback
      • 일반적 Transactional : 코드 실행이 다 끝난 후 Commit
    • andExpect : mvc를 통해 함수가 실행되고 난 결과를 예상 : 맞으면 통과, 틀리면 에러 발생
// 컨트롤러 통합 테스트 @Transactional @AutoConfigureMockMvc // MockMvc 클래스가 IoC에 로드 | RestTemplate -> 자바스크립트의 fetch와 동일, 진짜 환경에 요청 가능 @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK) // MOCK -> 가짜 환경을 만들어 필요한 의존관계를 다 메모리에 올려서 테스트 public class UserControllerTest { @Autowired private ObjectMapper om; // json <-> java Object 변환 해주는 객체. IoC에 objectMapper가 이미 떠있음 @Autowired private MockMvc mvc; // 가짜 환경에서 fetch 요청하는 클래스 | RestTemplate -> 자바스크립트의 fetch와 동일, 진짜 환경에 요청 가능 private String accessToken; @BeforeEach public void setUp() { // 테스트 시작 전에 실행할 코드 System.out.println("setUp"); User ssar = User.builder() .id(1) .username("ssar") .build(); accessToken = JwtUtil.create(ssar); } @AfterEach public void tearDown() { // 끝나고 나서 마무리 함수 // 테스트 후 정리할 코드 System.out.println("tearDown"); } @Test public void join_test() throws Exception { // 이 메서드를 호출한 주체에게 예외 위임 -> 지금은 jvm 이다 // given -> 가짜 데이터 UserRequest.JoinDTO reqDTO = new UserRequest.JoinDTO(); reqDTO.setEmail("haha@nate.com"); reqDTO.setPassword("1234"); reqDTO.setUsername("haha"); String requestBody = om.writeValueAsString(reqDTO); // System.out.println(requestBody); // {"username":"haha","password":"1234","email":"haha@nate.com"} // when -> 테스트 실행 ResultActions actions = mvc.perform( // 주소가 틀리면 터지고, json 아닌거 넣으면 터지고, 타입이 달라도 터지고. 따라서 미리 터진다고 알려줌 MockMvcRequestBuilders .post("/join") .content(requestBody) .contentType(MediaType.APPLICATION_JSON) ); // eye -> 결과 눈으로 검증 String responseBody = actions.andReturn().getResponse().getContentAsString(); //System.out.println(responseBody); // {"status":200,"msg":"성공","body":{"id":4,"username":"haha","email":"haha@nate.com","createdAt":"2025-05-13 11:45:23.604577"}} // then -> 결과를 코드로 검증 // json의 최상위 객체를 $ 표기한다 actions.andExpect(MockMvcResultMatchers.jsonPath("$.status").value(200)); actions.andExpect(MockMvcResultMatchers.jsonPath("$.msg").value("성공")); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.id").value(4)); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.username").value("haha")); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.email").value("haha@nate.com")); } @Test public void login_test() throws Exception { // given UserRequest.LoginDTO reqDTO = new UserRequest.LoginDTO(); reqDTO.setUsername("ssar"); reqDTO.setPassword("1234"); String requestBody = om.writeValueAsString(reqDTO); // System.out.println(requestBody); // when ResultActions actions = mvc.perform( MockMvcRequestBuilders .post("/login") .content(requestBody) .contentType(MediaType.APPLICATION_JSON) ); // eye String responseBody = actions.andReturn().getResponse().getContentAsString(); System.out.println(responseBody); // then (jwt 길이만 검증) -> 길이 변환 가능. 패턴만 확인 actions.andExpect(MockMvcResultMatchers.jsonPath("$.status").value(200)); actions.andExpect(MockMvcResultMatchers.jsonPath("$.msg").value("성공")); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.accessToken", matchesPattern("^[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+$"))); } @Test public void update_test() throws Exception { // given UserRequest.UpdateDTO reqDTO = new UserRequest.UpdateDTO(); reqDTO.setEmail("ssar@gmail.com"); reqDTO.setPassword("1234"); String requestBody = om.writeValueAsString(reqDTO); // System.out.println(requestBody); // when ResultActions actions = mvc.perform( MockMvcRequestBuilders .put("/s/api/user") .content(requestBody) .contentType(MediaType.APPLICATION_JSON) .header("Authorization", "Bearer " + accessToken) ); // eye String responseBody = actions.andReturn().getResponse().getContentAsString(); System.out.println(responseBody); // then actions.andExpect(MockMvcResultMatchers.jsonPath("$.status").value(200)); actions.andExpect(MockMvcResultMatchers.jsonPath("$.msg").value("성공")); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.id").value(1)); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.username").value("ssar")); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.email").value("ssar@gmail.com")); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.createdAt", matchesPattern("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d+"))); } @Test public void checkUsernameAvailable_test() throws Exception { // given String username = "ssar"; // when ResultActions actions = mvc.perform( MockMvcRequestBuilders .get("/api/check-username-available/{username}", username) ); // eye String responseBody = actions.andReturn().getResponse().getContentAsString(); System.out.println(responseBody); // then actions.andExpect(MockMvcResultMatchers.jsonPath("$.status").value(200)); actions.andExpect(MockMvcResultMatchers.jsonPath("$.msg").value("성공")); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.available").value(false)); } }
 
  • BoardControllerTest
    • 테스트에 사용할 DB와 실제 배포를 진행할 때 사용할 DB는 구조는 동일하되 값이 달라야 함
    • 실제 환경에 맞출 수 있도록 테스트 전용으로 사용하는 DB를 teardown이라고 함
    • teardown은 SQL 코드 내에 모든 데이터 값을 초기화시키는 코드를 포함해야 함 : Rollback
// SpringBootTest 내에는 @Transactional 미포함 : rollback을 위해 추가 -> 근본적 문제 해결 불가 @Transactional //@Sql("classpath:db/teardown.sql") - 테스트용 DB 데이터셋 사용 : 지금은 미진행 @AutoConfigureMockMvc @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK) public class BoardControllerTest { @Autowired private ObjectMapper om; @Autowired private MockMvc mvc; private String accessToken; @BeforeEach public void setUp() { // 테스트 시작 전에 실행할 코드 System.out.println("setUp"); User ssar = User.builder() .id(1) .username("ssar") .build(); accessToken = JwtUtil.create(ssar); } @AfterEach public void tearDown() { // 끝나고 나서 마무리 함수 // 테스트 후 정리할 코드 System.out.println("tearDown"); } @Test public void list_test() throws Exception { // given Integer page = 1; String keyword = "제목1"; // when ResultActions actions = mvc.perform( MockMvcRequestBuilders .get("/") .param("page", page.toString()) .param("keyword", keyword) ); // eye String responseBody = actions.andReturn().getResponse().getContentAsString(); System.out.println(responseBody); // then actions.andExpect(MockMvcResultMatchers.jsonPath("$.status").value(200)); actions.andExpect(MockMvcResultMatchers.jsonPath("$.msg").value("성공")); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.boards[0].id").value(16)); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.boards[0].title").value("제목16")); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.boards[0].content").value("내용16")); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.boards[0].isPublic").value(true)); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.boards[0].userId").value(1)); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.boards[0].createdAt", matchesPattern("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d+"))); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.prev").value(0)); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.next").value(2)); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.current").value(1)); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.size").value(3)); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.totalCount").value(11)); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.totalPage").value(4)); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.isFirst").value(false)); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.isLast").value(false)); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.numbers", hasSize(4))); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.keyword").value("제목1")); } @Test public void getBoardDetail_test() throws Exception { // given Integer id = 4; // when ResultActions actions = mvc.perform( MockMvcRequestBuilders .get("/api/board/{id}/detail", id) ); // eye String responseBody = actions.andReturn().getResponse().getContentAsString(); System.out.println(responseBody); // then actions.andExpect(MockMvcResultMatchers.jsonPath("$.status").value(200)); actions.andExpect(MockMvcResultMatchers.jsonPath("$.msg").value("성공")); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.id").value(4)); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.title").value("제목4")); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.content").value("내용4")); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.isPublic").value(true)); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.isBoardOwner").value(false)); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.isLove").value(false)); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.loveCount").value(2)); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.username").value("love")); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.loveId").value(nullValue())); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.replies[0].id").value(3)); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.replies[0].content").value("댓글3")); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.replies[0].username").value("ssar")); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.replies[0].isReplyOwner").value(false)); } @Test public void save_test() throws Exception { // given BoardRequest.SaveDTO reqDTO = new BoardRequest.SaveDTO(); reqDTO.setTitle("제목21"); reqDTO.setContent("내용21"); reqDTO.setIsPublic(true); String requestBody = om.writeValueAsString(reqDTO); // System.out.println(requestBody); // when ResultActions actions = mvc.perform( MockMvcRequestBuilders .post("/s/api/board") .content(requestBody) .contentType(MediaType.APPLICATION_JSON) .header("Authorization", "Bearer " + accessToken) ); // eye String responseBody = actions.andReturn().getResponse().getContentAsString(); System.out.println(responseBody); // then actions.andExpect(MockMvcResultMatchers.jsonPath("$.status").value(200)); actions.andExpect(MockMvcResultMatchers.jsonPath("$.msg").value("성공")); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.id").value(21)); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.title").value("제목21")); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.content").value("내용21")); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.isPublic").value(true)); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.userId").value(1)); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.createdAt", matchesPattern("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d+"))); } @Test public void getBoardOne_test() throws Exception { // given Integer id = 1; // when ResultActions actions = mvc.perform( MockMvcRequestBuilders .get("/s/api/board/{id}", id) .header("Authorization", "Bearer " + accessToken) ); // eye String responseBody = actions.andReturn().getResponse().getContentAsString(); System.out.println(responseBody); // then actions.andExpect(MockMvcResultMatchers.jsonPath("$.status").value(200)); actions.andExpect(MockMvcResultMatchers.jsonPath("$.msg").value("성공")); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.id").value(1)); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.title").value("제목1")); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.content").value("내용1")); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.isPublic").value(true)); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.userId").value(1)); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.createdAt", matchesPattern("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d+"))); } @Test public void update_test() throws Exception { // given Integer id = 1; BoardRequest.UpdateDTO reqDTO = new BoardRequest.UpdateDTO(); reqDTO.setTitle("제1"); reqDTO.setContent("내1"); reqDTO.setIsPublic(true); String requestBody = om.writeValueAsString(reqDTO); // System.out.println(requestBody); // when ResultActions actions = mvc.perform( MockMvcRequestBuilders .put("/s/api/board/{id}", id) .content(requestBody) .contentType(MediaType.APPLICATION_JSON) .header("Authorization", "Bearer " + accessToken) ); // eye String responseBody = actions.andReturn().getResponse().getContentAsString(); System.out.println(responseBody); // then actions.andExpect(MockMvcResultMatchers.jsonPath("$.status").value(200)); actions.andExpect(MockMvcResultMatchers.jsonPath("$.msg").value("성공")); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.id").value(1)); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.title").value("제1")); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.content").value("내1")); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.isPublic").value(true)); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.userId").value(1)); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.createdAt", matchesPattern("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d+"))); } }
 
  • ReplyControllerTest
@Transactional @AutoConfigureMockMvc @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK) public class ReplyControllerTest { @Autowired private ObjectMapper om; @Autowired private MockMvc mvc; private String accessToken; @BeforeEach public void setUp() { // 테스트 시작 전에 실행할 코드 System.out.println("setUp"); User ssar = User.builder() .id(1) .username("ssar") .build(); accessToken = JwtUtil.create(ssar); } @AfterEach public void tearDown() { // 끝나고 나서 마무리 함수 // 테스트 후 정리할 코드 System.out.println("tearDown"); } @Test public void save_test() throws Exception { // given ReplyRequest.SaveDTO reqDTO = new ReplyRequest.SaveDTO(); reqDTO.setBoardId(1); reqDTO.setContent("댓글6"); String requestBody = om.writeValueAsString(reqDTO); System.out.println(requestBody); // when ResultActions actions = mvc.perform( MockMvcRequestBuilders .post("/s/api/reply") .content(requestBody) .contentType(MediaType.APPLICATION_JSON) .header("Authorization", "Bearer " + accessToken) ); // eye String responseBody = actions.andReturn().getResponse().getContentAsString(); System.out.println(responseBody); // then actions.andExpect(MockMvcResultMatchers.jsonPath("$.status").value(200)); actions.andExpect(MockMvcResultMatchers.jsonPath("$.msg").value("성공")); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.id").value(6)); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.content").value("댓글6")); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.userId").value(1)); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.boardId").value(1)); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.createdAt", matchesPattern("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d+"))); } @Test public void delete_test() throws Exception { // given Integer id = 1; // when ResultActions actions = mvc.perform( MockMvcRequestBuilders .delete("/s/api/reply/{id}", id) .header("Authorization", "Bearer " + accessToken) ); // eye String responseBody = actions.andReturn().getResponse().getContentAsString(); System.out.println(responseBody); // then actions.andExpect(MockMvcResultMatchers.jsonPath("$.status").value(200)); actions.andExpect(MockMvcResultMatchers.jsonPath("$.msg").value("성공")); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body").value(nullValue())); } }
 
  • LoveControllerTest
@Transactional @AutoConfigureMockMvc @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK) public class LoveControllerTest { @Autowired private ObjectMapper om; @Autowired private MockMvc mvc; private String accessToken; @BeforeEach public void setUp() { // 테스트 시작 전에 실행할 코드 System.out.println("setUp"); User ssar = User.builder() .id(1) .username("ssar") .build(); accessToken = JwtUtil.create(ssar); } @AfterEach public void tearDown() { // 끝나고 나서 마무리 함수 // 테스트 후 정리할 코드 System.out.println("tearDown"); } @Test public void saveLove_test() throws Exception { // given LoveRequest.SaveDTO reqDTO = new LoveRequest.SaveDTO(); reqDTO.setBoardId(3); String requestBody = om.writeValueAsString(reqDTO); // System.out.println(requestBody); // when ResultActions actions = mvc.perform( MockMvcRequestBuilders .post("/s/api/love") .content(requestBody) .contentType(MediaType.APPLICATION_JSON) .header("Authorization", "Bearer " + accessToken) ); // eye String responseBody = actions.andReturn().getResponse().getContentAsString(); System.out.println(responseBody); // then actions.andExpect(MockMvcResultMatchers.jsonPath("$.status").value(200)); actions.andExpect(MockMvcResultMatchers.jsonPath("$.msg").value("성공")); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.loveId").value(4)); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.loveCount").value(1)); } @Test public void deleteLove_test() throws Exception { // given Integer id = 1; // when ResultActions actions = mvc.perform( MockMvcRequestBuilders .delete("/s/api/love/{id}", id) .header("Authorization", "Bearer " + accessToken) ); // eye String responseBody = actions.andReturn().getResponse().getContentAsString(); System.out.println(responseBody); // then actions.andExpect(MockMvcResultMatchers.jsonPath("$.status").value(200)); actions.andExpect(MockMvcResultMatchers.jsonPath("$.msg").value("성공")); actions.andExpect(MockMvcResultMatchers.jsonPath("$.body.loveCount").value(0)); } }
 

Result

  • 정상 작동 시 오류 검출 없음
notion image
 
  • 에러 검출 시 해당 테스트 부분의 오류 검출
notion image
 
Share article

sxias