1. 로컬 컴퓨터에서 잘 돌아가는 것이 운영 서버에서 잘 돌아가는지 확인하기 위함
- test 서버에서 자동으로 test가 되고 문제 없으면 실제 서버에 배포한다
- 문제가 생기면 빌드가 실패하여 jar 파일(자바 실행 파일)이 생성되지 않는다
- 만약 실행할 서버가 5개 라면 test서버가 없을 때 5개의 서버 각각 테스트를 해야 한다.
- 빌드만 하면 자동 test가 실행된다
- 빌드 프로그램이 빌드 할 때 자동으로 test 코드를 실행 해준다
- 로컬에서 잘 돌아가면 test 코드를 작성함
- 내가 하나하나 기능을 다 실행할 필요가 없다 = postman으로 하나하나 실행 해보는 것은 미친 짓이다
2. 코드 수정 후 의존 관계 문제를 확인하기 위함
의존 관계 문제
- 여러 의존 관계를 맺고 있는 어떤 객체를 수정하면 테스트를 하지 않고 그냥 빌드 할 때 연관되어있는 다른 코드들이 다 터진다
- 의존 관계가 어떻게 있는지 모르는 코드를 수정할 때는 수정 후 테스트를 꼭 실행시켜봐야 한다
- 테스트를 실행 시켜야 이 코드와 연결되어 있는 다른 코드에서 발생하는 에러를 찾을 수 있다
이론
1. 개발 과정

- 각자의 컴퓨터로 개발을 하고 github 에 통합 시킨다
- 계속되는 수정 보완이 발생하기 때문에 지속적으로 개발을 하고 통합을 시킨다
✅ CI (지속적 통합)란?
- Continuous Integration: 지속적인 코드 통합 및 자동 테스트
- 개발자들이 코드를 GitHub 와 같은 원격 저장소에 푸시할 때마다
자동으로 빌드 및 테스트가 수행되어 안정성을 확보
- 보통
v1.0
,v1.1
처럼 버전 태깅을 통해 변경 이력을 관리함
- CI의 핵심 목표: "로컬에서만 잘 돌아가는 코드" → "모든 환경에서 안정적으로 작동하도록"
2. 개발이 끝나면 테스트를 해본다
1. 로컬 테스트를 해봐야 함
- 개발 환경 → 윈도우, jdk21, mysql

- CD → 지속적 배포
- 코드를 수정하고 서비스 서버에 계속 다시 배포한다
- AWS 환경에서 잘 동작하는지 확인 해야 한다
- github 에서 받아서 빌드하고 실행한다
- postman 으로 테스트 해본다(모든 주소 실행)

- 문제가 생기면 다시 코드를 짜고 github 에 올린다
- 다시 1번으로 돌아간다
⚠️ 로컬에서 잘 되더라도 서버에선 잘 안 될 수 있다?
로컬 환경 = 개인 개발 환경 (Windows, 내 JDK, 내 DB)서버 환경 = 운영 환경 (리눅스, 다른 설정, 클라우드 IP 등)
- 로컬에서는 모든 게 내 기준으로 돌아감 → 운영 서버와는 환경이 다름
- 그래서 "로컬에선 되는데 서버에선 안 됨" 현상이 잦음
- 이를 해결하기 위해 필요한 것이 테스트 서버 + 배포 자동화 시스템
3. 직접 테스트를 하는 것은 너무 힘들다
1. 통합 테스트 코드를 작성해서 처리한다
- postman 을 사용하여 하나하나 테스트를 하는 것은 너무 힘들 일이다
- 테스트 코드를 작성하여 자동으로 확인되게 만들어야 한다
- Controller 통합 테스트 코드를 작성한다
- 테스트 코드가 포함된 코드를 서비스 서버에 배포하면 빌드할 때 자동으로 테스트 코드가 실행된다
- 대부분의 빌드 프로그램들은 빌드 시 자동으로 테스트 코드가 실행된다
- 에러가 발생하면 빌드가 취소된다
- 여기서 에러가 발생하면 빌드는 실패하고 배포가 되지 않는다
- 배포에 실패하면 다시 코드를 수정하고 서버에 배포한다
2. 테스트 서버를 만들어야 하는 이유

테스트 서버가 없다면
- 매번 새로 배포할 때마다 서버가 재시작된다.
- 테스트 코드가 없다면 배포 완료 후 테스트를 하고 문제가 생기면 다시 배포한다. 이때마다 서버는 껐다 켜야 한다
- 잘 돌아가는 원래 서버에 수정된 코드를 올려서 배포하고 테스트를 하면 테스트를 하는 동안 원래 서버는 꺼진 상태다. 서비스 불가
- 서버가 여러 대라면 각각의 서버를 모두 테스트해야 한다.
테스트 서버가 있다면
- 테스트를 위해 잘 돌아가는 원래의 서버를 끌 필요가 없다
- 이 서버에 배포한 뒤 테스트를 실행하고 문제가 없으면 바로 본 서비스 서버에 배포한다
- 서버가 여러 대라도 한번만 테스트를 실행하면 된다
실습
통합 테스트에서 테스트하기 힘든 유형
- 날짜
- 토큰 값
통합 테스트에서 컨트롤러 테스트는 filter → dispatcher → controller 로 들어오는 구조로 실행된다
1. 통합 테스트 패키지 생성

2. 테스트 코드 작성
1. 테스트 클래스 세팅
package shop.mtcoding.blog.integre;
// 컨트롤러 통합 테스트
@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와 동일, 진짜 환경에 요청 가능
}
@AutoConfigureMockMvc
→ 컨트롤러 통합 테스트 어노테이션MockMvc
객체가IoC
에 로드된다- gpt 설명 추가
- 가상 환경에서 HTTP 요청하는 객체
RestTemplate
-> 자바스크립트의 fetch와 동일, 실제 환경에 요청 가능
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
→ MOCK : 가상 환경을 만들어 필요한 의존 관계를 다 메모리에 올려준다- gpt 설명 추가
@Autowired
→ 타입을 기준으로 스프링이 의존성 주입 해줌
ObjectMapper
→ json ↔ java Object 변환 해주는 객체@RestController
→ 를 사용할 때 자동으로 json 으로 변환 해주는 객체임- 이미 IoC에 로드 되어 있음. new 할 필요 없음
MockMvc
→ 가상의 환경에서 HTTP 요청하는 객체
2. 테스트 메서드 작성
- 컨벤션 → 실제 사용되는 <메서드이름 + _test>
- 컨벤션을 잘 지켰다면 api 문서는 자동으로 만들어준다
@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"));
}
throws Exception
→ 이 메서드를 호출한 주체에게 예외 처리 위임. 여기선 jvm 이다
// given -> 가짜 데이터
UserRequest.JoinDTO reqDTO = new UserRequest.JoinDTO();
reqDTO.setEmail("haha@nate.com");
reqDTO.setPassword("1234");
reqDTO.setUsername("haha");
- 가상의
reqDTO
를 만들어 given 데이터로 사용한다
String requestBody = om.writeValueAsString(reqDTO);
System.out.println(requestBody); // {"username":"haha","password":"1234","email":"haha@nate.com"}
om.writeValueAsString
→ java Object 를 json 으로 변환
System.out.println
→ 꼭 눈으로 확인하자

// when -> 테스트 실행
ResultActions actions = mvc.perform( // 주소가 틀리면 터지고, json 아닌거 넣으면 터지고, 타입이 달라도 터지고. 따라서 미리 터진다고 알려줌
MockMvcRequestBuilders
.post("/join")
.content(requestBody)
.contentType(MediaType.APPLICATION_JSON)
);
perform
→ 가상 요청을 실행, checkedException 으로 예외 문제를 미리 빨간 줄로 알려줌
- 요청 메서드 선택
.get("/api/board/{id}/detail", id)
.post("/s/api/board")
.put("/s/api/board/{id}", id)
.delete("/s/api/reply/{id}", id)
.content(requestBody)
→ body 데이터 넣기
.contentType(MediaType.APPLICATION_JSON)
→ header의 mime-type 선택
- 토큰을 넣고 싶다면
ResultActions actions = mvc.perform(
MockMvcRequestBuilders
.delete("/s/api/reply/{id}", id)
.header("Authorization", "Bearer " + accessToken)
);
.header("Authorization", "Bearer " + accessToken)
→ 토큰 넣는 방법// 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"}}
actions.andReturn().getResponse().getContentAsString();
→ 요청의 응답을 json 으로 변환
System.out.println(responseBody);
→ 눈으로 꼭 확인. 확인하고actions.andExpect
코드를 작성해야 한다

// 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"));
- 눈으로 확인한
actions
의 로그 값을 보고 하나하나 작성한다
- 해당 패키지 안에 있는 모든 테스트 코드가 한번에 실행 된다
3. 테스트 코드 실행 전, 실행 후 처리하고 싶은 것이 있다면…
@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");
}
}
@BeforeEach
→ 이 어노테이션을 사용하는 메서드는 모든 테스트 코드가 실행되기 전에 실행된다- 토큰과 같은 컨트롤러 테스트 전에 필요한 데이터는 이 방법으로 만들어서 사용한다
@AfterEach
→ 이 어노테이션을 사용하는 메서드는 모든 테스트 코드가 실행된 후 실행된다
4. 통합 테스트


3. 테스트 코드 작성 중 에러
유효성 검증 체크에서 에러발생

다음 에러가 발생한다면

⬇ 변경

@NotEmpty
어노테이션은 문자열, 컬렉션, 배열 등 "비어 있을 수 있는 타입"에만 적용 가능한데, 지금Integer
타입에 잘못 사용되었기 때문입니다.
통합 테스트 실행 후 에러 발생(트랜젝션 에러)
- 개별 실행 시 문제가 없었다
- 통합으로 한번에 실행하니 문제가 발생
getBoardOne_test 실행 중 발생
{"status":200,"msg":"성공","body":{"id":1,"title":"제1","content":"내1","isPublic":true,"userId":1,"createdAt":"2025-05-13 18:10:15.689923"}}
tearDown
MockHttpServletRequest:
HTTP Method = GET
Request URI = /s/api/board/1
Parameters = {}
Headers = [Authorization:"Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJibG9nIiwiaWQiOjEsImV4cCI6MTc0NzEzMTAxNiwidXNlcm5hbWUiOiJzc2FyIn0.bmzGfs7tBr701URWZcFp439sCCa94vC9qyqrBIMoN-rGcqiU_sFbGAGHxnaopb9Tuvho_FutBDsGuJ3m-uij7g"]
Body = null
Session Attrs = {sessionUser=shop.mtcoding.blog.user.User@e60c516}
Handler:
Type = shop.mtcoding.blog.board.BoardController
Method = shop.mtcoding.blog.board.BoardController#getBoardOne(int)
Async:
Async started = false
Async result = null
Resolved Exception:
Type = null
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
Attributes = null
MockHttpServletResponse:
Status = 200
Error message = null
Headers = [Vary:"Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers", Content-Type:"application/json;charset=UTF-8"]
Content type = application/json;charset=UTF-8
Body = {"status":200,"msg":"성공","body":{"id":1,"title":"제1","content":"내1","isPublic":true,"userId":1,"createdAt":"2025-05-13 18:10:15.689923"}}
Forwarded URL = null
Redirected URL = null
Cookies = []
JSON path "$.body.title"
Expected :제목1
Actual :제1
<Click to see difference>
java.lang.AssertionError: JSON path "$.body.title" expected:<제목1> but was:<제1>
- 예상되는 값은 “제목1” 인데 실제 나온 값은 “제1” 이다
- 이유는 update 테스트를 실행한 뒤에 이 테스트가 실행이 되어 db에 데이터가 변경 되었기 때문이다
- 각 테스트는 db 를 건드린 다음에는 롤백을 해야한다

- 이 어노테이션을 붙인 뒤 문제 해결

@Transactional
→ test 코드에서는 자동 rollback 된다
- ❗db에서 자동 id증가하는 메서드를 초기화 할 수 없다
@Sql("classpath:db/teardown.sql")
테스트 파일들
BoardControllerTest
package shop.mtcoding.blog.integre;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.transaction.annotation.Transactional;
import shop.mtcoding.blog._core.util.JwtUtil;
import shop.mtcoding.blog.board.BoardRequest;
import shop.mtcoding.blog.user.User;
import static org.hamcrest.Matchers.*;
@Transactional
@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+")));
}
}
UserControllerTest
package shop.mtcoding.blog.integre;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.transaction.annotation.Transactional;
import shop.mtcoding.blog._core.util.JwtUtil;
import shop.mtcoding.blog.user.User;
import shop.mtcoding.blog.user.UserRequest;
import static org.hamcrest.Matchers.matchesPattern;
@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));
}
}
ReplyControllerTest
package shop.mtcoding.blog.integre;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.transaction.annotation.Transactional;
import shop.mtcoding.blog._core.util.JwtUtil;
import shop.mtcoding.blog.reply.ReplyRequest;
import shop.mtcoding.blog.user.User;
import static org.hamcrest.Matchers.matchesPattern;
import static org.hamcrest.Matchers.nullValue;
@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
package shop.mtcoding.blog.integre;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.transaction.annotation.Transactional;
import shop.mtcoding.blog._core.util.JwtUtil;
import shop.mtcoding.blog.love.LoveRequest;
import shop.mtcoding.blog.user.User;
@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));
}
}
서비스 테스트 할 때
좋은 질문입니다.
@Mock
과 @MockBean
은 모두 Mockito를 이용해 Mock 객체를 만들기 위한 어노테이션이지만, 적용 범위와 목적이 다릅니다.✅ @Mock
vs @MockBean
차이 정리
구분 | @Mock | @MockBean |
목적 | 단순 Mockito Mock 객체 생성 | Spring 컨텍스트 안의 Bean을 Mock으로 교체 |
사용 대상 | 단위 테스트 (JUnit + Mockito) | SpringBootTest 통합 테스트 |
동작 방식 | 단순한 가짜 객체 생성 | 스프링 컨테이너에 있는 실제 Bean을 가짜로 덮어쓰기 |
Spring Context 필요 여부 | ❌ 필요 없음 | ✅ Spring Context 필요 |
주로 사용되는 상황 | 순수 단위 테스트 (Service만 테스트) | 통합 테스트 (Service + Bean 간 연동 포함) |
✅ 예시 코드 비교
1. @Mock
(단위 테스트에서)
@ExtendWith(MockitoExtension.class)
public class LottoServiceUnitTest {
@Mock
private LottoRepository lottoRepository;
@InjectMocks
private LottoService lottoService;
@Test
public void someTest() {
when(lottoRepository.findAll()).thenReturn(List.of());
lottoService.doSomething();
verify(lottoRepository).findAll();
}
}
- MockitoExtension 사용
- Spring Context 로딩 안 함
- Service와 내부 Mock만 테스트
2. @MockBean
(SpringBootTest에서)
@SpringBootTest
public class LottoSchedulerServiceTest {
@Autowired
private LottoSchedulerService lottoSchedulerService;
@MockBean
private LottoRepository lottoRepository;
@Test
public void someTest() {
when(lottoRepository.findAll()).thenReturn(List.of());
lottoSchedulerService.checkWinners();
verify(lottoRepository).findAll();
}
}
- Spring Context 실제로 로딩됨
- Spring Bean 등록된
LottoRepository
를 Mock으로 대체
- Service 뿐 아니라 연관된 실제 Bean 흐름까지 테스트 가능
💡 결론
@Mock
→ Mockito 기반 단위 테스트용- 빠름, 가볍고 간단함
@MockBean
→ Spring Boot 통합 테스트에서 기존 빈을 Mock으로 대체- 전체 흐름을 통제하면서 내부 의존성 행위를 검증할 때 적합
Share article