💡 통합 테스트 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
- 정상 작동 시 오류 검출 없음

- 에러 검출 시 해당 테스트 부분의 오류 검출

Share article