🧩 단위 테스트란?
단위 테스트
- 정의: 작은 단위로 쪼개서 각 단위가 정확하게 동작하는지를 검사하는 테스트 기법
- 장점: 빠르게 작성할 수 있고 문제 발생 시 어느 부분이 잘못 되었는지를 빠르고 정확하게 확인할 수 있다.
- 필요성: 테스트 코드를 작성하면 프로그램의 버그를 사전에 발견하여 기하급수적인 비용의 증가가능성을 사전에 방지할 수 있다.
- JUnit5: 자바 프로그래밍 언어용 단위 테스트 프레임워크
JUnit5 다루기
build.gradle: default로 가지고 있기 때문에 따로 건드릴 필요는 없다.
dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test'
}
tasks.named('test') {
useJUnitPlatform()
}
BeforeAfterTest
package com.sparta.myselectshop;
import org.junit.jupiter.api.*;
public class BeforeAfterTest {
@BeforeEach
void setUp() {
System.out.println("각각의 테스트 코드가 실행되기 전에 수행");
}
@AfterEach
void tearDown() {
System.out.println("각각의 테스트 코드가 실행된 후에 수행");
}
@BeforeAll
static void beforeAll() { // static으로 만들어야 함
System.out.println("모든 테스트 코드가 실행되기 전에 최초로 수행");
}
@AfterAll
static void afterAll() {
System.out.println("모든 테스트 코드가 수행된 후 마지막으로 수행");
}
@Test
void test1() {
System.out.println("테스트 코드 1");
}
@Test
void test2() {
System.out.println("테스트 코드 2");
}
}

- 테스트 꾸미기
@DisplayName: 테스트 이름 설정@Nested: 테스트 그룹핑@Order: 테스트 순서 설정
@Nested
@DisplayName("첫 번째 테스트")
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class Test1 {
@Order(2)
@Test
@DisplayName("테스트 1-1")
void test1() {
System.out.println("Test1.test1");
}
@Order(1)
@Test
@DisplayName("테스트 1-2")
void test2() {
System.out.println("Test1.test2");
}
}
@Nested
@DisplayName("두 번째 테스트")
class Test2 {
@Test
@DisplayName("테스트 2-1")
void test1() {
System.out.println("Test2.test1");
}
@Test
@DisplayName("테스트 2-2")
void test2() {
System.out.println("Test2.test2");
}
}

- 테스트 반복하기
@RepeatedTest(value = 5, name = "반복 테스트 {currentRepetition} / {totalRepetitions}")
void repeatTest(RepetitionInfo info) {
System.out.println("테스트 반복 : " + info.getCurrentRepetition() + " / " + info.getTotalRepetitions());
}

@DisplayName("파라미터 값 활용하여 테스트 하기")
@ParameterizedTest
@ValueSource(ints = {1, 2, 3, 4, 5, 6, 7, 8, 9})
void parameterTest(int num) {
System.out.println("5 * num = " + 5 * num);
}

- Assertion
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
public class AssertionTest {
Calculator calculator;
@BeforeEach
void setUp() {
calculator = new Calculator();
}
@Test
@DisplayName("assertEquals")
void test1() {
Double result = calculator.operate(5, "/", 2);
assertEquals(2.5, result);
}
@Test
@DisplayName("assertEquals - Supplier")
void test1_1() {
Double result = calculator.operate(5, "/", 0);
// 테스트 실패 시 메시지 출력 (new Supplier<String>())
assertEquals(2.5, result, () -> "연산자 혹은 분모가 0이 아닌지 확인해보세요!");
}
@Test
@DisplayName("assertNotEquals")
void test1_2() {
Double result = calculator.operate(5, "/", 0);
assertNotEquals(2.5, result);
}
}

@Test
@DisplayName("assertTrue와 assertFalse")
void test2() {
assertTrue(calculator.validateNum(9));
assertFalse(calculator.validateNum(0));
}
@Test
@DisplayName("assertNotNull과 assertNull")
void test3() {
Double result1 = calculator.operate(5, "/", 2);
assertNotNull(result1);
Double result2 = calculator.operate(5, "/", 0);
assertNull(result2);
}
@Test
@DisplayName("assertThrows")
void test4() {
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> calculator.operate(5, "?", 2));
assertEquals("잘못된 연산자입니다.", exception.getMessage());
}
- 보다 더 상세한 설명은 아래 공식 Junit 문서에서 확인할 수 있다.
Overview :: JUnit User Guide
The goal of this document is to provide comprehensive reference documentation for programmers writing tests, extension authors, and engine authors as well as build tool and IDE vendors.
docs.junit.org
Mock Object
- 각 테스트 케이스를 서로 분리시키기 위해 생성하는 가짜 객체
- 분리가 어려운 클래스들: Controller/Service/Repository 클래스만 테스트할 수는 없을까?

- MockController/Service/Repository: 실제 객체와 겉만 같은 객체이지만 실제 DB 작업은 하지 않음
Mockito 사용하기
- Mockito: Mock 객체를 쉽게 만들 수 있는 방법을 제공하는 프레임워크
ProductServiceTest
▼ 코드 보기@ExtendWith(MockitoExtension.class) class ProductServiceTest { @Mock ProductRepository productRepository; @Mock FolderRepository folderRepository; @Mock ProductFolderRepository productFolderRepository; @Test @DisplayName("관심 상품 희망가 - 최저가 이상으로 변경") void test1() { // given Long productId = 100L; int myprice = ProductService.MIN_MY_PRICE + 3_000_000; ProductMypriceRequestDto requestMyPriceDto = new ProductMypriceRequestDto(); requestMyPriceDto.setMyprice(myprice); ProductService productService = new ProductService(productRepository, folderRepository, productFolderRepository); // when ProductResponseDto result = productService.updateProduct(productId, requestMyPriceDto); // then assertEquals(myprice, result.getMyprice()); } @Test @DisplayName("관심 상품 희망가 - 최저가 미만으로 변경") void test2() { // given Long productId = 200L; int myprice = ProductService.MIN_MY_PRICE - 50; ProductMypriceRequestDto requestMyPriceDto = new ProductMypriceRequestDto(); requestMyPriceDto.setMyprice(myprice); ProductService productService = new ProductService(productRepository, folderRepository, productFolderRepository); // when Exception exception = assertThrows(IllegalArgumentException.class, () -> { productService.updateProduct(productId, requestMyPriceDto); }); // then assertEquals( "유효하지 않은 관심 가격입니다. 최소 " +ProductService.MIN_MY_PRICE + " 원 이상으로 설정해 주세요.", exception.getMessage() ); } }

ProductServiceTest수정: Mock 사용 케이스 추가
▼ 코드 보기@ExtendWith(MockitoExtension.class) class ProductServiceTest { @Mock ProductRepository productRepository; @Mock FolderRepository folderRepository; @Mock ProductFolderRepository productFolderRepository; @Test @DisplayName("관심 상품 희망가 - 최저가 이상으로 변경") void test1() { // given Long productId = 100L; int myprice = ProductService.MIN_MY_PRICE + 3_000_000; ProductMypriceRequestDto requestMyPriceDto = new ProductMypriceRequestDto(); requestMyPriceDto.setMyprice(myprice); User user = new User(); ProductRequestDto requestProductDto = new ProductRequestDto( "Apple <b>맥북</b> <b>프로</b> 16형 2021년 <b>M1</b> Max 10코어 실버 (MK1H3KH/A) ", "https://shopping-phinf.pstatic.net/main_2941337/29413376619.20220705152340.jpg", "https://search.shopping.naver.com/gate.nhn?id=29413376619", 3515000 ); Product product = new Product(requestProductDto, user); ProductService productService = new ProductService(productRepository, folderRepository, productFolderRepository); given(productRepository.findById(productId)).willReturn(Optional.of(product)); // when ProductResponseDto result = productService.updateProduct(productId, requestMyPriceDto); // then assertEquals(myprice, result.getMyprice()); } @Test @DisplayName("관심 상품 희망가 - 최저가 미만으로 변경") void test2() { // given Long productId = 200L; int myprice = ProductService.MIN_MY_PRICE - 50; ProductMypriceRequestDto requestMyPriceDto = new ProductMypriceRequestDto(); requestMyPriceDto.setMyprice(myprice); ProductService productService = new ProductService(productRepository, folderRepository, productFolderRepository); // when Exception exception = assertThrows(IllegalArgumentException.class, () -> { productService.updateProduct(productId, requestMyPriceDto); }); // then assertEquals( "유효하지 않은 관심 가격입니다. 최소 " +ProductService.MIN_MY_PRICE + "원 이상으로 설정해 주세요.", exception.getMessage() ); } }
🧩 통합 테스트란?
단위 테스트 vs 통합 테스트
| 단위 테스트 | 통합 테스트 |
| ▪ 하나의 모듈/클래스에 대해 세밀한 부분까지 테스트가 가능하다. ▪ 모듈 간에 상호 작용 검증은 할 수 없다. |
▪ 두 개 이상의 모듈이 연결된 상태를 테스트할 수 있다. ▪ 모듈 간의 연결에서 발생하는 에러 검증 가능하다. |
Spring Boot를 이용한 통합 테스트
- 통합 테스트
- 여러 단위 테스트를 하나의 통합된 테스트로 수행한다.
- 단위 테스트 시 Spring은 동작되지 않는다.
@SpringBootTest- 스프링이 동작되도록 해주는 어노테이션
- 테스트 수행 시 스프링이 동작한다.
- Spring IoC/DI 기능을 사용 가능하다.
- Repository를 사용해 DB CRUD가 가능하다.
ProductServiceIntegrationTest
▼ 코드 보기@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(MethodOrderer.OrderAnnotation.class) class ProductServiceIntegrationTest { @Autowired ProductService productService; @Autowired UserRepository userRepository; User user; ProductResponseDto createdProduct = null; int updatedMyPrice = -1; @Test @Order(1) @DisplayName("신규 관심상품 등록") void test1() { // given String title = "Apple <b>에어팟</b> 2세대 유선충전 모델 (MV7N2KH/A)"; String imageUrl = "https://shopping-phinf.pstatic.net/main_1862208/18622086330.20200831140839.jpg"; String linkUrl = "https://search.shopping.naver.com/gate.nhn?id=18622086330"; int lPrice = 173900; ProductRequestDto requestDto = new ProductRequestDto( title, imageUrl, linkUrl, lPrice ); user = userRepository.findById(1L).orElse(null); // when ProductResponseDto product = productService.createProduct(requestDto, user); // then assertNotNull(product.getId()); assertEquals(title, product.getTitle()); assertEquals(imageUrl, product.getImage()); assertEquals(linkUrl, product.getLink()); assertEquals(lPrice, product.getLprice()); assertEquals(0, product.getMyprice()); createdProduct = product; } @Test @Order(2) @DisplayName("신규 등록된 관심상품의 희망 최저가 변경") void test2() { // given Long productId = this.createdProduct.getId(); int myPrice = 173000; ProductMypriceRequestDto requestDto = new ProductMypriceRequestDto(); requestDto.setMyprice(myPrice); // when ProductResponseDto product = productService.updateProduct(productId, requestDto); // then assertNotNull(product.getId()); assertEquals(this.createdProduct.getTitle(), product.getTitle()); assertEquals(this.createdProduct.getImage(), product.getImage()); assertEquals(this.createdProduct.getLink(), product.getLink()); assertEquals(this.createdProduct.getLprice(), product.getLprice()); assertEquals(myPrice, product.getMyprice()); this.updatedMyPrice = myPrice; } @Test @Order(3) @DisplayName("회원이 등록한 모든 관심상품 조회") void test3() { // given // when Page<ProductResponseDto> productList = productService.getProducts(user, 0, 10, "id", false); // then // 1. 전체 상품에서 테스트에 의해 생성된 상품 찾아오기 (상품의 id 로 찾음) Long createdProductId = this.createdProduct.getId(); ProductResponseDto foundProduct = productList.stream() .filter(product -> product.getId().equals(createdProductId)) .findFirst() .orElse(null); // 2. Order(1) 테스트에 의해 생성된 상품과 일치하는지 검증 assertNotNull(foundProduct); assertEquals(this.createdProduct.getId(), foundProduct.getId()); assertEquals(this.createdProduct.getTitle(), foundProduct.getTitle()); assertEquals(this.createdProduct.getImage(), foundProduct.getImage()); assertEquals(this.createdProduct.getLink(), foundProduct.getLink()); assertEquals(this.createdProduct.getLprice(), foundProduct.getLprice()); // 3. Order(2) 테스트에 의해 myPrice 가격이 정상적으로 업데이트되었는지 검증 assertEquals(this.updatedMyPrice, foundProduct.getMyprice()); } }

Cannot lazily initialize collection of role 'com.sparta.myselectshop.entity.Product.productFolderList' with key '167' (no session)
- 테스트 결과
LazyInitializationException에러가 발생했다.- 원인: 영속성 컨텍스트가 종료된 상태에서 지연 로딩(Lazy Loading)으로 설정된 연관 엔티티나 컬렉션을 참조하려고 했다.
- 해결:
ProductService의getProducts메서드에@Transactional(readOnly = true)어노테이션을 추가해서 DTO로 변환이 끝날 때까지 DB 세션을 열어두도록 만들었다.

※ 테스트 결과 하위 목록에 아무것도 뜨지 않는다면, 바로 위 툴바에서 ✅ 아이콘을 클릭해보자.

🧩 Controller 테스트
MockSpringSecurityFilter
public class MockSpringSecurityFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) {}
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
SecurityContextHolder.getContext()
.setAuthentication((Authentication) ((HttpServletRequest) req).getUserPrincipal());
chain.doFilter(req, res);
}
@Override
public void destroy() {
SecurityContextHolder.clearContext();
}
}
UserProductMvcTest
▼ 코드 보기@WebMvcTest( controllers = {UserController.class, ProductController.class}, excludeFilters = { @ComponentScan.Filter( type = FilterType.ASSIGNABLE_TYPE, classes = SecurityConfig.class ) } ) class UserProductMvcTest { private MockMvc mvc; private Principal mockPrincipal; @Autowired private WebApplicationContext context; // @Autowired // private ObjectMapper objectMapper; private final ObjectMapper objectMapper = new ObjectMapper(); @MockitoBean JwtUtil jwtUtil; @MockitoBean UserService userService; @MockitoBean KakaoService kakaoService; @MockitoBean ProductService productService; @MockitoBean FolderService folderService; @BeforeEach public void setup() { mvc = MockMvcBuilders.webAppContextSetup(context) .apply(springSecurity(new MockSpringSecurityFilter())) .build(); } private void mockUserSetup() { String username = "sollertia4351"; String password = "robbie1234"; String email = "sollertia@sparta.com"; UserRoleEnum role = UserRoleEnum.USER; User testUser = new User(username, password, email, role); UserDetailsImpl testUserDetails = new UserDetailsImpl(testUser); mockPrincipal = new UsernamePasswordAuthenticationToken(testUserDetails, "", testUserDetails.getAuthorities()); } @Test @DisplayName("로그인 Page") void test1() throws Exception { // when - then mvc.perform(get("/api/user/login-page")) .andExpect(status().isOk()) .andExpect(view().name("login")) .andDo(print()); } @Test @DisplayName("회원 가입 요청 처리") void test2() throws Exception { // given MultiValueMap<String, String> signupRequestForm = new LinkedMultiValueMap<>(); signupRequestForm.add("username", "sollertia4351"); signupRequestForm.add("password", "robbie1234"); signupRequestForm.add("email", "sollertia@sparta.com"); signupRequestForm.add("admin", "false"); // when - then mvc.perform(post("/api/user/signup") .params(signupRequestForm) ) .andExpect(status().is3xxRedirection()) .andExpect(view().name("redirect:/api/user/login-page")) .andDo(print()); } @Test @DisplayName("신규 관심상품 등록") void test3() throws Exception { // given this.mockUserSetup(); String title = "Apple <b>아이폰</b> 14 프로 256GB [자급제]"; String imageUrl = "https://shopping-phinf.pstatic.net/main_3456175/34561756621.20220929142551.jpg"; String linkUrl = "https://search.shopping.naver.com/gate.nhn?id=34561756621"; int lPrice = 959000; ProductRequestDto requestDto = new ProductRequestDto( title, imageUrl, linkUrl, lPrice ); String postInfo = objectMapper.writeValueAsString(requestDto); // when - then mvc.perform(post("/api/products") .content(postInfo) .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON) .principal(mockPrincipal) ) .andExpect(status().isOk()) .andDo(print()); } }
이 상태에서 실행하면 아래와 같은 에러가 발생할 것이다.
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'jpaAuditingHandler': Cannot resolve reference to bean 'jpaMappingContext' while setting constructor argument
MySelectShopApplication:@EnableJpaAuditing주석 처리JpaConfig생성
@Configuration
@EnableJpaAuditing
public class JpaConfig {
}

'내일배움캠프' 카테고리의 다른 글
| [내일배움캠프] 예외 처리 (0) | 2026.04.10 |
|---|---|
| [내일배움캠프] Spring AOP (0) | 2026.04.10 |
| [내일배움캠프] 카카오 소셜 로그인 구현 (0) | 2026.04.10 |
| [내일배움캠프] RestTemplate, Open API (0) | 2026.04.09 |
| [내일배움캠프] 필터, Spring Security, Validation (0) | 2026.04.08 |