[Spring] Spring Boot 테스트 코드 작성하기 – JUnit5, Mockito, MockMvc 구현
by 우와한개발자
1. 테스트 코드를 작성하는 이유
- 기능을 개발한 뒤 동작 여부를 확인하려면 보통 서버를 켜고 Postman으로 직접 요청을 보냄.
- 그런데 이 방식은 매번 서버를 켜야 하고, 코드를 수정했을 때 기존 기능이 깨졌는지 일일이 확인하기 어려움.
- 테스트 코드를 작성하면 서버를 켜지 않아도 자동으로 검증할 수 있고, 테스트 자체가 코드의 동작을 설명하는 문서 역할도 함.
2. 테스트 종류
테스트 종류
├── 단위 테스트 (@ExtendWith(MockitoExtension.class))
│ └── 하나의 클래스만 독립 테스트, Spring 없이 실행 → 매우 빠름
│ ex) DashboardServiceTest
│
├── 슬라이스 테스트 (@WebMvcTest)
│ └── Controller 레이어만 Spring 로드 → 빠름
│ ex) AdminControllerTest, MemberViewControllerTest
│
└── 통합 테스트 (@SpringBootTest)
└── 전체 Spring 컨텍스트 로드, 실제 DB 연결 → 느림
ex) DashboardCachePerformanceTest
3. 의존성
- spring-boot-starter-test 하나만 추가하면 JUnit 5, Mockito, AssertJ, MockMvc가 모두 포함됨
- <scope>test</scope>로 선언하면 테스트 실행 시에만 사용되고, 실제 빌드 결과물(jar)에는 포함되지 않음.
<!-- JUnit5 + Mockito + AssertJ + MockMvc 전부 포함 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Spring Security를 사용하는 경우 추가 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
| 라이브러리 | 역할 |
| JUnit 5 | 테스트 실행 및 관리 (뼈대) |
| Mockito | 가짜 객체(Mock) 생성 |
| AssertJ | 결과값 검증 |
| MockMvc | HTTP 요청/응답 테스트 |
| spring-security-test | @WithMockUser 등 Security 테스트 지원 |
4. JUnit 5 : 테스트의 뼈대
- 테스트를 실행하고 관리하는 프레임워크. 어떤 메서드가 테스트인지 선언하고, 실행 순서를 제어하며, 결과를 보여주는 역할.
| 어노테이션 | 실행 시점 | 설명 |
| @Test | - | 이 메서드가 테스트임을 선언 |
| @BeforeEach | 각 테스트 전 | 테스트마다 공통 초기화 |
| @AfterEach | 각 테스트 후 | 테스트마다 공통 정리 |
| @BeforeAll | 전체 시작 전 1번 | 클래스 전체에서 한 번만 실행 |
| @DisplayName | - | 테스트 이름을 한글로 표시 |
| @Disabled | - | 해당 테스트를 임시 비활성화 |
5. AssertJ : 결과 검증 담당
- 테스트 결과가 기대한 값과 일치하는지 검증하는 라이브러리.
- JUnit에도 assertEquals 같은 검증 메서드가 있지만 AssertJ는 영어 문장처럼 읽혀서 가독성이 훨씬 좋음.
// JUnit 방식 — 순서가 헷갈리고 읽기 어려움
assertEquals(5, result);
// AssertJ 방식 — 자연스러운 영어 문장처럼 읽힘
assertThat(result).isEqualTo(5);
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
// 값 비교
assertThat(result).isEqualTo(5);
assertThat(name).isEqualTo("홍길동");
// null 여부
assertThat(result).isNotNull();
// 리스트 검증
assertThat(list).hasSize(3);
assertThat(list).containsExactly("2024-01", "2024-02");
assertThat(list).isEmpty();
// 예외 발생 검증
assertThatThrownBy(() -> service.getById(-1L))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("존재하지 않는");
6. Mockito : 가짜 객체(Mock) 생성 담당
- DashboardService를 테스트하려면 DashboardRepository가 필요하고, DashboardRepository는 DB가 있어야 동작함.
- 테스트할 때마다 DB에 연결하면 느리고 테스트 데이터가 DB에 쌓이는 등 여러 문제가 생김.
- 이 문제를 해결하기 위해 Repository를 가짜(Mock)로 교체.
실제 구조: DashboardService → DashboardRepository → Oracle DB
테스트 구조: DashboardService → Mock Repository (DB 없음, 가짜 반환값)
@ExtendWith(MockitoExtension.class) // Mockito 활성화
class DashboardServiceTest {
@Mock // 가짜 Repository 생성 (DB 연결 없음)
DashboardRepository dashboardRepository;
@InjectMocks // 가짜 Repository를 Service에 자동 주입
DashboardService dashboardService;
@Test
void KPI_조회_성공() {
// given: 가짜 Repository가 반환할 값 미리 설정
BikeKpi fakeKpi = new BikeKpi();
fakeKpi.setTotalUseCnt(1000L);
when(dashboardRepository.selectLatestKpi()) // "이 메서드가 호출되면"
.thenReturn(fakeKpi); // "이 값을 반환해"
// when
BikeKpiSummaryResponseDto result = dashboardService.getKpi();
// then
assertThat(result.getTotalUseCnt()).isEqualTo(1000L);
}
}
// 반환값 설정
when(mock.메서드()).thenReturn(반환값);
// 예외 발생 설정
when(mock.메서드()).thenThrow(new RuntimeException("오류"));
// 이 메서드가 실제로 호출됐는지 검증
verify(mock).메서드();
verify(mock, times(2)).메서드(); // 2번 호출됐는지
verify(mock, never()).메서드(); // 한 번도 호출 안 됐는지
7. MockMvc : Controller HTTP 테스트 담당
- 실제 서버를 켜지 않고도 HTTP 요청을 테스트할 수 있게 해주는 도구.
- GET /api/v1/dashboard/kpi를 호출했을 때 200이 오는지, 응답 JSON이 올바른지 등을 검증할 수 있음.
- @WebMvcTest는 Controller 레이어만 Spring에 로드하고 Service는 Mock으로 대체하기 때문에 @SpringBootTest보다 빠르게 실행됨.
8. 테스트 구현
1) @Mock vs @MockBean 차이
- Spring을 띄우지 않는 단위 테스트에서는 @Mock 사용
- Spring 컨텍스트를 사용하는 슬라이스/통합 테스트에서는 @MockBean 사용
| 어노테이션 | 사용 위치 | 설명 |
| @Mock | @ExtendWith(MockitoExtension.class) | 순수 Mockito 환경에서 가짜 객체 생성 |
| @MockBean | @WebMvcTest, @SpringBootTest | Spring 컨텍스트에 가짜 빈으로 등록 |
2) AdminControllerTest : 슬라이스 테스트 (권한 검증)
- Controller가 올바른 뷰를 반환하는지, 권한이 없을 때 403이 오는지 검증.
- @WithMockUser로 특정 Role을 가진 가짜 사용자를 주입해서 권한 분기를 테스트 가능.
@WebMvcTest(AdminController.class)
@Import({ SecurityConfig.class, AdminControllerTest.TestConfig.class })
public class AdminControllerTest {
@TestConfiguration
@EnableMethodSecurity
static class TestConfig { }
@Autowired
private MockMvc mockMvc;
@MockitoBean
private IAdminService adminService;
@MockitoBean
private MemberAuthService memberAuthService;
@MockitoBean
private JwtTokenProvider jwtTokenProvider;
@MockitoBean
private JwtCookieUtils jwtCookieUtils;
@Test
@WithMockUser(username = "adminUser", roles = { "ADMIN" })
@DisplayName("관리자 마이페이지 접근 테스트 - 권한이 있는 경우")
void adminMyPageAccessSuccess() throws Exception {
// given
Member mockMember = Member.builder()
.userId(1L)
.loginId("adminUser")
.userName("관리자")
.build();
AdminDashboardDto mockDashboardData = new AdminDashboardDto();
mockDashboardData.setAdminName("관리자");
mockDashboardData.setPendingCount(5);
// ...
given(memberAuthService.getMember("adminUser")).willReturn(mockMember);
given(adminService.getDashboardData(1L, 0, 10)).willReturn(mockDashboardData);
// when & then
mockMvc.perform(get("/admin/mypage"))
.andDo(print())
.andExpect(status().isOk())
.andExpect(view().name("admin/mypage"))
.andExpect(model().attribute("adminName", "관리자"))
.andExpect(model().attribute("pendingCount", 5))
.andExpect(model().attribute("activeMenu", "mypage"));
}
@Test
@WithMockUser(username = "normalUser", roles = { "USER" })
@DisplayName("관리자 마이페이지 접근 테스트 - 권한이 없는 경우(403)")
void adminMyPageAccessDenied() throws Exception {
mockMvc.perform(get("/admin/mypage"))
.andDo(print())
.andExpect(status().isForbidden()); // 403 확인
}
}
3) MemberViewControllerTest — 슬라이스 테스트 (리디렉션 검증)
- 로그인/비로그인 상태에 따라 올바른 페이지로 이동하는지 검증
@WebMvcTest(MemberViewController.class)
@Import(SecurityConfig.class)
class MemberViewControllerTest {
@Autowired
MockMvc mockMvc;
@MockitoBean
MemberAuthService memberAuthService;
@MockitoBean
IUploadFileService uploadFileService;
@Test
@DisplayName("GET /login - 비로그인 사용자 → 로그인 페이지 반환")
void loginPage_비로그인_로그인페이지() throws Exception {
mockMvc.perform(get("/login"))
.andExpect(status().isOk())
.andExpect(view().name("member/login"));
}
@Test
@WithMockUser
@DisplayName("GET /login - 로그인 사용자 → 대시보드로 리디렉션")
void loginPage_로그인_대시보드리디렉션() throws Exception {
mockMvc.perform(get("/login"))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/dashboard"));
}
@Test
@WithMockUser(username = "testUser")
@DisplayName("GET /mypage - 로그인 사용자 → 마이페이지 반환")
void myPage_로그인_마이페이지() throws Exception {
Member fakeMember = Member.builder()
.userId(1L)
.loginId("testUser")
.userName("홍길동")
.email("test@test.com")
.build();
when(memberAuthService.getMember("testUser")).thenReturn(fakeMember);
when(uploadFileService.countFilesByUploaderId(1L)).thenReturn(0);
when(uploadFileService.getFilesByUploaderIdPaged(1L, 0, 5)).thenReturn(List.of());
mockMvc.perform(get("/mypage"))
.andExpect(status().isOk())
.andExpect(view().name("mypage/user-mypage"))
.andExpect(model().attribute("userName", "홍길동"))
.andExpect(model().attribute("userEmail", "test@test.com"));
}
@Test
@DisplayName("GET /mypage - 비로그인 사용자 → 로그인 페이지로 리디렉션")
void myPage_비로그인_로그인리디렉션() throws Exception {
mockMvc.perform(get("/mypage"))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrlPattern("**/login"));
}
}
4) DashboardCachePerformanceTest : 통합 테스트 (캐시 성능 검증)
- @SpringBootTest로 전체 Spring 컨텍스트를 로드해서 실제 DB 조회와 캐시 반환 시간을 비교.
@SpringBootTest
class DashboardCachePerformanceTest {
@Autowired
private DashboardService dashboardService;
@Autowired
private CacheManager cacheManager;
@BeforeEach
void clearCache() {
// 테스트 간 캐시 초기화 (이전 테스트 결과가 영향을 주지 않도록)
cacheManager.getCacheNames().forEach(name -> {
var cache = cacheManager.getCache(name);
if (cache != null) cache.clear();
});
}
@Test
@DisplayName("[캐시 성능 비교] KPI 요약")
void compareKpiPerformance() {
comparePerformance("KPI 요약", () -> dashboardService.getKpi());
}
private void comparePerformance(String label, Runnable callable) {
long s1 = System.nanoTime();
callable.run(); // 1차 호출 : DB 조회 (Cache Miss)
long noCacheNs = System.nanoTime() - s1;
long s2 = System.nanoTime();
callable.run(); // 2차 호출 : 캐시 반환 (Cache Hit)
long cachedNs = System.nanoTime() - s2;
double ratio = cachedNs > 0 ? (double) noCacheNs / cachedNs : 0;
System.out.printf(
"[%s] 1차(DB): %.3f ms | 2차(Cache): %.3f ms | 속도향상: %.2f배%n",
label,
noCacheNs / 1_000_000.0,
cachedNs / 1_000_000.0,
ratio
);
}
}
9. Given-When-Then 패턴
- 모든 테스트는 Given-When-Then 3단계로 작성하는 것이 표준
- 코드만 봐도 무엇을 테스트하는지 바로 파악할 수 있어서 팀 협업 시 유용.
Given (준비): 테스트에 필요한 데이터와 Mock 설정
When (실행): 실제로 테스트할 메서드 호출
Then (검증): 결과가 기대한 값과 일치하는지 확인
@Test
@DisplayName("월별 통계 조회 시 month, useCount 리스트를 반환한다")
void getMonthlySummary_성공() {
// given
List<BikeMonthlySummary> fakeList = List.of(
createSummary("2024-01", 5000L),
createSummary("2024-02", 6000L)
);
when(dashboardRepository.selectMonthlySummaryList()).thenReturn(fakeList);
// when
BikeMonthlySummaryResponseDto result = dashboardService.getMonthlySummary();
// then
assertThat(result.getMonthList()).hasSize(2);
assertThat(result.getMonthList()).containsExactly("2024-01", "2024-02");
assertThat(result.getUseCountList()).containsExactly(5000L, 6000L);
}
10. CI에서 테스트 자동화
- DB 연결이 필요한 통합 테스트는 CI 환경에서 실행 불가능하기 때문에, GitHub Actions에서는 해당 테스트를 제외하고 실행.
- !클래스명으로 특정 테스트를 제외하고, 나머지 단위/슬라이스 테스트만 CI에서 자동으로 실행되도록 구성.
# .github/workflows/maven.yml
- name: Build and Test with Maven
run: ./mvnw test -Dtest="!MyddareungiApplicationTests,!DashboardCachePerformanceTest"
'프레임워크 > Spring' 카테고리의 다른 글
| [Spring] Spring Cache란? @Cacheable로 DB 조회 성능 개선 (0) | 2026.04.12 |
|---|---|
| [ Spring ] Spring Boot 배포 방식 – JAR vs WAR, 외부 톰캣 배포 (0) | 2026.04.07 |
| [Spring] SSE(Server-Sent Events)란? EventSource, SseEmitter 구현 (1) | 2026.04.07 |
| [Spring] WebSocket이란? 스프링 프로젝트 웹소켓 구현 (0) | 2026.04.07 |
| [Spring] Spring HATEOAS란? – EntityModel, ModelAssembler 구현 (0) | 2026.04.07 |
블로그의 정보
우와한개발자 님의 블로그
우와한개발자