JUnit은 자바 프로그래밍에서 테스트를 수행하는 데 널리 사용되는 프레임워크 중 하나이다.
JUnit을 사용하면 단위 테스트를 작성하고 실행하여 소스 코드 모듈이 의도한 대로 작동하는지 확인할 수 있다. 특히, JUnit 5에서는 모듈화된 구조로 테스트 작성 및 실행을 지원하여 개발자가 효율적으로 테스트할 수 있다.
소프트웨어 시스템의 여러 구성 요소가 함께 작동하여 예상대로 작동하는지 확인하는 것은 통합 테스트에 있어 중요한 단계이다. 이를 위해 Spring Boot는 MockMvc와 Mockito 같은 테스트 프레임워크를 제공하여 웹 애플리케이션을 테스트하고 의존성이 있는 객체를 가짜 객체로 대체하여 테스트를 수행할 수 있다.
JUnit과 Mockito를 활용하여 테스트를 하면서 프로젝트 설정 오류부터 코드 오류까지, 발생한 오류를 하나씩 해결해 나가면서 테스트를 진행해 보도록 하자.
💡JUnit
JUnit은 Java에서 가장 많이 사용되는 테스트 프레임워크 중 하나로, 독립된 단위테스트를 지원한다.
JUnit은 테스트 작성자를 위한 API 모듈과 테스트 실행을 위한 API를 분리하여 제공하며, 현재 가장 많이 사용되는 버전은 JUnit 5이다.
JUnit 모듈
- Unit Platform: 테스트를 발견하고 테스트 계획을 생성하는 역할을 한다.
- JUnit Jupiter: JUnit 5에서 테스트 및 확장을 위한 API를 제공하며, 테스트 코드 작성에 필요한 기능을 제공한다.
- JUnit Vintage: JUnit 3 및 JUnit 4와 호환성을 유지하기 위한 모듈이다.
JUnit은 크게 위의 세 가지 모듈로 구성된다.
단위 테스트(Unit Test)
단위 테스트는 특정 소스 코드의 모듈이 의도한 대로 작동하는지를 검증하는 테스트이다.
주로 클래스 또는 메서드 단위로 작성되며, 해당 부분만 독립적으로 테스트하기 때문에 코드를 리팩터링 하거나 수정할 때 빠르게 문제를 파악할 수 있다. 특 히, Spring에서의 단위 테스트는 Spring Container에 올라와 있는 Bean을 테스트하는 것이다.
개발자는 코드 변경 사항이나 새로운 기능을 추가할 때 이전 기능이 올바르게 작동하는지 확인하며 작성한 테스트 코드를 실행하여 문제점을 빠르게 파악할 수 있다.
통합 테스트(Integration Test)
통합 테스트는 소프트웨어 시스템의 여러 구성 요소가 함께 작동하여 예상대로 작동하는지 확인하는 테스트이다.
통합 테스트는 주로 서로 다른 모듈, 클래스, 또는 시스템 간의 상호 작용을 검증한다.
통합 테스트의 주요 구성 요소
- MockMvc: Spring 프레임워크에서 제공하는 가짜 웹 MVC 프레임워크로, 웹 애플리케이션을 테스트할 수 있다.
- Mockito: Java에서 가짜 객체를 생성하고 행동을 지정하여 테스트하는 데 사용되는 모의(Mocking) 프레임워크이다.
- @MockBean: Spring Boot에서 Mock 객체를 생성하고 주입하는 데 사용되는 어노테이션이다.
- @AutoConfigureMockMvc: Spring Boot 테스트에서 MockMvc 인스턴스를 자동으로 구성하는 데 사용되는 어노테이션이다.
- @SpringBootTest: Spring Boot 애플리케이션의 전체 컨텍스트를 로드하여 통합 테스트를 수행하는 데 사용된다.
Given-When-Then Pattern
Given-When-Then은 시나리오를 [준비 - 실행 - 검증] 단계로 나누는 것이다.
Given-When-Then 패턴을 사용하면 테스트를 보다 명확하게 구성할 수 있고, 테스트의 의도를 명확하게 전달할 수 있다.
기능 : 사용자 주식 트레이드
시나리오 : 트레이드가 마감되기 전에 사용자가 판매를 요청
"Given" 나는 MSFT 주식을 100가지고 있다. 그리고 나는 APPL 주식을 150가지고 있다. 그리고 시간은 트레이드가 종료되기 전이다.
"When" 나는 MSFT 주식 20을 팔도록 요청했다.
"Then" 나는 MSFT 주식 80 가지고 있어야 한다. 그리고 나는 APPL 주식 150을 가지고 있어야 한다. 그리고 MSFT 주식 20이 판매 요청이 실행되었어야 한다.
Given
Given은 테스트를 준비하는 과정을 나타낸다.
이 부분에서는 테스트를 위해 필요한 모든 조건과 상태를 설정한다. 보통은 테스트에 필요한 객체를 생성하고 설정하거나, 테스트 데이터를 준비하는 등의 작업을 수행한다.
이 부분이 테스트의 초기 설정을 담당하므로, Given 부분을 통해 테스트의 전체적인 맥락을 파악할 수 있다.
When
When은 테스트의 액션 또는 행동을 나타낸다.
테스트를 실행하고자 하는 특정 동작을 이 부분에서 수행한다. Given 부분에서 설정한 상황을 바탕으로 어떤 행동을 취할지를 명시한다. 즉, 시스템에 어떤 입력이 주어지거나, 어떤 이벤트가 발생할 때를 의미한다.
Then
Then은 테스트의 결과를 검증하는 부분이다.
When 부분에서 수행한 액션에 대한 결과가 기대한 대로 나왔는지를 확인하고 검증한다. 즉, 시스템이 어떻게 반응해야 하는지를 명시하고, 그에 대한 검증을 수행한다.
이 부분에서는 예상된 결과를 실제 결과와 비교하고, 결과가 일치하는지 여부를 확인한다.
💡Mock과 Mockito
Mock은 테스트 중에 실제 객체를 대신하여 동작을 제어할 수 있는 가짜 객체를 의미한다.
Mockito는 Java 개발자가 Mock 객체를 쉽게 만들고 사용할 수 있도록 도와주는 테스트 프레임워크이다.
Mockito를 사용하면 의존성이 있는 객체를 가짜 객체로 대체하여 단위 테스트를 진행할 수 있다.
테스트에서 사용하는 어노테이션
Mockito에서는 다양한 기능을 제공하는 어노테이션이 있지만, 주로 테스트에서 가짜 객체를 사용할 때 다음의 세 가지 어노테이션을 활용한다.
@Mock
가짜 객체를 만들어 반환해주는 어노테이션이다. 이 어노테이션은 목(Mock) 객체를 생성하여 해당 객체를 주입하는 데 사용된다.
@Spy
Stub하지 않은 메서드들은 원본 메서드 그대로 사용하는 어노테이션이다. 즉, 일부 메서드만 행동을 변경하고자 할 때 사용된다.
이 어노테이션은 일부 메소드를 실제로 호출하고 원본 객체의 동작을 유지하면서 다른 메서드는 가짜 동작을 제공할 수 있다.
@InjectMocks
@Mock 또는 @Spy로 생성된 가짜 객체를 자동으로 주입시켜주는 어노테이션이다.
이 어노테이션은 목 객체를 포함하는 테스트 대상 객체를 주입할 때 사용된다.
Mock과 Mockito 사용 예시
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import static org.mockito.Mockito.*;
public class UserControllerTest {
@Mock
private UserService userService; // UserService의 가짜 객체를 생성
@InjectMocks
private UserController userController; // UserController에 가짜 UserService 주입
@BeforeEach
public void setUp() {
// Mockito 어노테이션을 초기화하여 사용 가능하게 함
MockitoAnnotations.initMocks(this);
}
@Test
public void testUserController() {
// UserService의 특정 메소드를 호출하면서 가짜 동작을 지정
when(userService.getUserById(1L)).thenReturn(new User(1L, "John"));
// UserController의 메소드 호출
String userName = userController.getUserNameById(1L);
// 예상된 결과와 실제 결과 비교
assertEquals("John", userName);
}
}
예를 들어, UserController에 대한 단위 테스트를 작성할 때 UserService를 사용한다고 가정하면, 이와 같이 UserController와 UserService 사이의 의존성을 해결하기 위해 Mockito의 @Mock 어노테이션과 @InjectMocks 어노테이션을 사용할 수 있다.
위의 예시에서는 UserController에 대한 단위 테스트를 작성하고 있다. UserController는 UserService를 의존하고 있으며, 이를 해결하기 위해 @Mock 어노테이션을 사용하여 가짜 UserService 객체를 생성하고, @InjectMocks 어노테이션을 사용하여 이를 UserController에 주입한다. 그런 다음 when(...).thenReturn(...)메서드를 사용하여 UserService의 메서드가 호출될 때 가짜 동작을 지정할 수 있다. 이렇게 하면 특정 메서드의 동작을 원하는 대로 테스트할 수 있게 된다.
❗오류 메시지 해결
The import org.junit cannot be resolved
프로젝트에 JUnit 라이브러리가 없어서 발생하는 오류이다.
Java Build Path에서 Add Library 버튼을 누르고, JUnit을 추가해 주면 오류가 해결된다.
https://stackoverflow.com/questions/15105556/the-import-org-junit-cannot-be-resolved
스택오버플로우에서는 이 오류에 대한 해결 방법으로 Java Build Path -> Libraries -> Add Library -> JUnit을 선택하거나 pom.xml 파일에 JUnit 의존성을 추가하는 것을 제시하고 있다.
The import org.springframework.boot.test cannot be resolved
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<!--<scope>test</scope>-->
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<!--<scope>test</scope>-->
</dependency>
pom.xml에서 추가한 의존성에서 <scope> 태그를 지워주면 오류가 해결된다.
https://stackoverflow.com/questions/52309445/springboottest-annotation-cannot-be-resolved-to-a-type
스택오버플로우에서는 Maven을 사용하는 경우 Spring Boot Starter Test 의존성을 추가할 때 <scope> 태그를 제거하거나 Spring Boot Starter Parent를 부모로 설정하여 의존성 버전을 관리하는 것을 제시하고 있다. 그리고 Gradle을 사용하는 경우 testImplementation 구성을 사용하여 Spring Boot Starter Test를 추가하는 것을 제시하고 있다.
The import org.mockito.Mockito cannot be resolved
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.12.4</version>
<!--<scope>test</scope>-->
</dependency>
마찬가지로 이 오류 또한 <scope> 태그를 지우자 오류가 사라졌다.
The input type of the launch configuration does not exist
테스트 클래스는 반드시 public으로 해야 한다는 점에 주의하도록 하자.
public으로 class를 수정하기 전에는 계속해서 위와 같은 오류가 발생해서 'Run all tests in the selected project, package or source folder'를 체크해서 프로젝트 통합 테스트를 진행해야 했다.
public으로 class를 재정의하고 나니 이러한 에러 메시지 없이 바로 단위 테스트가 진행되었다.
org.tymeleaf.exceptions.TemplateImputException
<meta name="_csrf" th:content="${_csrf.token}">
<meta name="_csrf_header" th:content="${_csrf.headerName}">
이 오류는 Thymeleaf 템플릿 파싱 중에 발생한 오류로, '_csrf.token' 표현식에서 'token' 프로퍼티나 필드를 찾을 수 없다는 오류이다.
@TestPropertySource(properties = {
"spring.thymeleaf.cache=false",
"spring.thymeleaf.enabled=false"
})
이를 해결하기 위해서는 Test Class에 위 코드를 추가하면 일시적으로 Thymeleaf 캐시를 비활성화하고 Thymeleaf를 비활성화하여 <meta> 태그를 템플릿에서 제거할 수 있다.
이렇게 하면 Thymeleaf 템플릿이 테스트 중에만 비활성화되므로 오류가 발생하지 않는다.
@SpringBootApplication(exclude = SecurityAutoConfiguration.class)
만약 Spring Security가 실행되어 오류가 발생한다면 위 코드를 MainApplication.java 파일에 추가하면 된다. 요청 헤더에 Base64로 인코딩 된 사용자 이름과 비밀번호를 포함시키는 방법도 있지만, 이 방법은 애초에 Spring Security가 실행되지 않게 한다.
💡테스트 실행 결과
통합 테스트(Integration Test)
package com.test.bank.benefit.controller;
import static org.mockito.Mockito.when;
import java.util.ArrayList;
import java.util.List;
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.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import com.test.bank.benefit.domain.BenefitDTO;
import com.test.bank.benefit.service.BenefitService;
/**
* UserBenefitController의 통합 테스트 클래스입니다.
*
* 이 테스트는 MockMvc와 Mockito를 사용하여 컨트롤러의 동작을 테스트합니다.
* BenefitService의 행위를 목(mock)하고, 컨트롤러의 동작을 검증합니다.
*
* /user/benefit/view.do 엔드포인트에 대한 GET 요청을 테스트하여 요청이 올바르게 처리되는지, 응답이 예상대로 반환되는지 확인합니다.
*
* @author 이승원
* @since 2024.02.28.
*/
@SpringBootTest
@AutoConfigureMockMvc
@TestPropertySource(properties = {
"spring.thymeleaf.cache=false",
"spring.thymeleaf.enabled=false"
})
public class UserBenefitControllerIntegrationTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private BenefitService benefitService;
/**
* view 메서드의 테스트입니다.
* view 메서드는 /user/benefit/view.do 엔드포인트에 대한 GET 요청을 처리합니다.
*
* 테스트는 다음 단계로 구성됩니다.
* 1. benefitService.getBenefits() 메서드가 호출될 때 반환되는 목(Mock) 데이터를 준비합니다.
* 2. mockMvc를 사용하여 /user/benefit/view.do 엔드포인트에 GET 요청을 수행합니다.
* 3. 요청의 응답을 검증하여 상태코드가 200(OK)인지, 뷰 이름이 "user/benefit/view"인지,
* 그리고 모델에 "benefitList" 속성이 존재하고 그 값이 이전에 준비한 목(Mock) 데이터와 일치하는지 확인합니다.
*
* @throws Exception 테스트 수행 중 예외가 발생할 경우
*/
@Test
void testView() throws Exception {
// Given
List<BenefitDTO> benefitList = new ArrayList<>();
BenefitDTO benefit1 = new BenefitDTO();
benefit1.setBenefitSeq(1);
benefit1.setBenefitName("특정 품목 현장할인");
benefit1.setContent("Samsung 제품의 특정 품목을 현장할인");
benefit1.setStartDate(java.sql.Timestamp.valueOf("2024-01-01 00:00:00"));
benefit1.setEndDate(java.sql.Timestamp.valueOf("2024-01-31 00:00:00"));
benefit1.setFranchiseSeq(1);
benefit1.setFranchiseName("Samsung");
benefit1.setTel("010-1264-5178");
benefitList.add(benefit1);
BenefitDTO benefit2 = new BenefitDTO();
benefit2.setBenefitSeq(2);
benefit2.setBenefitName("5% 청구할인");
benefit2.setContent("Naver에서 결제하면 5% 청구할인");
benefit2.setStartDate(java.sql.Timestamp.valueOf("2024-01-14 00:00:00"));
benefit2.setEndDate(java.sql.Timestamp.valueOf("2024-02-29 00:00:00"));
benefit2.setFranchiseSeq(4);
benefit2.setFranchiseName("네이버");
benefit2.setTel("010-4567-8501");
benefitList.add(benefit2);
benefitList.add(benefit1);
benefitList.add(benefit2);
// Mock service behavior
when(benefitService.getBenefits()).thenReturn(benefitList);
// When/Then
mockMvc.perform(MockMvcRequestBuilders.get("/user/benefit/view.do"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.view().name("user/benefit/view"))
.andExpect(MockMvcResultMatchers.model().attributeExists("benefitList"))
.andExpect(MockMvcResultMatchers.model().attribute("benefitList", benefitList));
}
}
23:22:02.952 [main] INFO org.springframework.test.context.support.AnnotationConfigContextLoaderUtils -- Could not detect default configuration classes for test class [com.test.bank.benefit.controller.UserBenefitControllerIntegrationTest]: UserBenefitControllerIntegrationTest does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
23:22:03.091 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper -- Found @SpringBootConfiguration com.test.bank.AtlanBankApplication for test class com.test.bank.benefit.controller.UserBenefitControllerIntegrationTest
23:22:03.226 [main] INFO org.springframework.boot.devtools.restart.RestartApplicationListener -- Restart disabled due to context in which it is running
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.2.1)
2024-02-28T23:22:03.454+09:00 INFO 20100 --- [ main] b.c.UserBenefitControllerIntegrationTest : Starting UserBenefitControllerIntegrationTest using Java 17.0.9 with PID 20100 (started by 이승원 in C:\Users\zhzkd\Documents\workspace-spring-tool-suite-4-4.21.0.RELEASE\atlan-bank)
2024-02-28T23:22:03.457+09:00 INFO 20100 --- [ main] b.c.UserBenefitControllerIntegrationTest : No active profile set, falling back to 1 default profile: "default"
2024-02-28T23:22:04.252+09:00 INFO 20100 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
2024-02-28T23:22:04.281+09:00 INFO 20100 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 19 ms. Found 0 JPA repository interfaces.
2024-02-28T23:22:05.198+09:00 INFO 20100 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2024-02-28T23:22:05.203+09:00 WARN 20100 --- [ main] c.zaxxer.hikari.util.DriverDataSource : Registered driver with driverClassName=oracle.jdbc.driver.OracleDriver was not found, trying direct instantiation.
2024-02-28T23:22:05.504+09:00 INFO 20100 --- [ main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection oracle.jdbc.driver.T4CConnection@3e01796a
2024-02-28T23:22:05.506+09:00 INFO 20100 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
2024-02-28T23:22:05.550+09:00 INFO 20100 --- [ main] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [name: default]
2024-02-28T23:22:05.610+09:00 INFO 20100 --- [ main] org.hibernate.Version : HHH000412: Hibernate ORM core version 6.4.1.Final
2024-02-28T23:22:05.656+09:00 INFO 20100 --- [ main] o.h.c.internal.RegionFactoryInitiator : HHH000026: Second-level cache disabled
2024-02-28T23:22:05.792+09:00 INFO 20100 --- [ main] o.s.o.j.p.SpringPersistenceUnitInfo : No LoadTimeWeaver setup: ignoring JPA class transformer
2024-02-28T23:22:05.893+09:00 WARN 20100 --- [ main] org.hibernate.dialect.Dialect : HHH000511: The 11.2.0 version for [org.hibernate.dialect.OracleDialect] is no longer supported, hence certain features may not work properly. The minimum supported version is 19.0.0. Check the community dialects project for available legacy versions.
2024-02-28T23:22:06.302+09:00 INFO 20100 --- [ main] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
2024-02-28T23:22:06.307+09:00 INFO 20100 --- [ main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2024-02-28T23:22:06.816+09:00 WARN 20100 --- [ main] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
2024-02-28T23:22:07.302+09:00 INFO 20100 --- [ main] o.s.b.t.m.w.SpringBootMockServletContext : Initializing Spring TestDispatcherServlet ''
2024-02-28T23:22:07.302+09:00 INFO 20100 --- [ main] o.s.t.web.servlet.TestDispatcherServlet : Initializing Servlet ''
2024-02-28T23:22:07.303+09:00 INFO 20100 --- [ main] o.s.t.web.servlet.TestDispatcherServlet : Completed initialization in 1 ms
2024-02-28T23:22:07.325+09:00 INFO 20100 --- [ main] b.c.UserBenefitControllerIntegrationTest : Started UserBenefitControllerIntegrationTest in 4.109 seconds (process running for 5.06)
2024-02-28T23:22:07.522+09:00 INFO 20100 --- [ionShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2024-02-28T23:22:07.525+09:00 INFO 20100 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown initiated...
2024-02-28T23:22:07.533+09:00 INFO 20100 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown completed.
통합 테스트는 소프트웨어의 여러 구성 요소 간의 상호 작용을 검증하는 것이다.
이 테스트에서는 MockMvc를 사용하여 컨트롤러를 호출하고, 실제 서비스 대신에 목(Mock) 서비스를 사용하여 컨트롤러와 서비스 간의 상호 작용을 테스트하고 있다.
테스트 결과 분석
1. 테스트는 총 0.156초(156밀리초) 소요되었다.
2. MockMvc를 사용하여 컨트롤러를 호출하고, 목(Mock) 서비스를 사용하여 컨트롤러와 서비스 간의 상호 작용을 테스트하였다.
3. 결과적으로 통합 테스트에서 예상된 대로 동작하고 있음을 확인할 수 있다.
단위 테스트(Unit Test)
package com.test.bank.benefit.controller;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestPropertySource;
import org.springframework.ui.Model;
import static org.mockito.Mockito.when;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertAll;
import com.test.bank.benefit.domain.BenefitDTO;
import com.test.bank.benefit.service.BenefitService;
/**
* 사용자 혜택 컨트롤러의 단위 테스트 클래스입니다.
*
* 이 테스트 클래스는 Mockito를 사용하여 컨트롤러의 동작을 테스트합니다.
*
* @author 이승원
* @since 2024.02.28.
*/
@SpringBootTest
@AutoConfigureMockMvc
@TestPropertySource(properties = {
"spring.thymeleaf.cache=false",
"spring.thymeleaf.enabled=false"
})
public class UserBenefitControllerUnitTest {
@InjectMocks
private UserBenefitController controller;
@Mock
private BenefitService service;
@Mock
private Model model;
@BeforeEach
public void setUp() {
// Mockito 초기화
MockitoAnnotations.initMocks(this);
}
@Test
public void testView() {
// Given
// 예상 혜택 목록 생성
List<BenefitDTO> expectedBenefitList = new ArrayList<>();
BenefitDTO benefit1 = new BenefitDTO();
benefit1.setBenefitSeq(1);
benefit1.setBenefitName("특정 품목 현장할인");
benefit1.setContent("Samsung 제품의 특정 품목을 현장할인");
benefit1.setStartDate(java.sql.Timestamp.valueOf("2024-01-01 00:00:00"));
benefit1.setEndDate(java.sql.Timestamp.valueOf("2024-01-31 00:00:00"));
benefit1.setFranchiseSeq(1);
benefit1.setFranchiseName("Samsung");
benefit1.setTel("010-1264-5178");
expectedBenefitList.add(benefit1);
// 서비스 동작 목 설정
when(service.getBenefits()).thenReturn(expectedBenefitList);
// 모델 동작 목 설정
when(model.getAttribute("benefitList")).thenReturn(expectedBenefitList);
// When
// 컨트롤러의 view 메서드 호출
String viewName = controller.view(model);
// Then
// 1. 반환된 뷰 이름이 예상과 일치하는지 확인
// 2. 혜택 목록 속성이 모델에 추가되었는지 확인
assertAll("View",
() -> assertEquals("user/benefit/view", viewName),
() -> {
// 혜택 목록 속성 값이 예상되는 값과 일치하는지 확인
List<BenefitDTO> actualBenefitList = (List<BenefitDTO>) model.getAttribute("benefitList");
assertEquals(expectedBenefitList.size(), actualBenefitList.size());
assertEquals(expectedBenefitList.get(0).getBenefitSeq(), actualBenefitList.get(0).getBenefitSeq());
assertEquals(expectedBenefitList.get(0).getBenefitName(), actualBenefitList.get(0).getBenefitName());
assertEquals(expectedBenefitList.get(0).getContent(), actualBenefitList.get(0).getContent());
assertEquals(expectedBenefitList.get(0).getBenefitImg(), actualBenefitList.get(0).getBenefitImg());
assertEquals(expectedBenefitList.get(0).getStartDate(), actualBenefitList.get(0).getStartDate());
assertEquals(expectedBenefitList.get(0).getEndDate(), actualBenefitList.get(0).getEndDate());
assertEquals(expectedBenefitList.get(0).getFranchiseSeq(), actualBenefitList.get(0).getFranchiseSeq());
assertEquals(expectedBenefitList.get(0).getFranchiseName(), actualBenefitList.get(0).getFranchiseName());
assertEquals(expectedBenefitList.get(0).getFranchiseImg(), actualBenefitList.get(0).getFranchiseImg());
assertEquals(expectedBenefitList.get(0).getTel(), actualBenefitList.get(0).getTel());
}
);
}
}
00:18:08.915 [main] INFO org.springframework.test.context.support.AnnotationConfigContextLoaderUtils -- Could not detect default configuration classes for test class [com.test.bank.benefit.controller.UserBenefitControllerUnitTest]: UserBenefitControllerUnitTest does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
00:18:09.041 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper -- Found @SpringBootConfiguration com.test.bank.AtlanBankApplication for test class com.test.bank.benefit.controller.UserBenefitControllerUnitTest
00:18:09.190 [main] INFO org.springframework.boot.devtools.restart.RestartApplicationListener -- Restart disabled due to context in which it is running
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.2.1)
2024-02-29T00:18:09.407+09:00 INFO 14580 --- [ main] c.t.b.b.c.UserBenefitControllerUnitTest : Starting UserBenefitControllerUnitTest using Java 17.0.9 with PID 14580 (started by 이승원 in C:\Users\zhzkd\Documents\workspace-spring-tool-suite-4-4.21.0.RELEASE\atlan-bank)
2024-02-29T00:18:09.409+09:00 INFO 14580 --- [ main] c.t.b.b.c.UserBenefitControllerUnitTest : No active profile set, falling back to 1 default profile: "default"
2024-02-29T00:18:10.245+09:00 INFO 14580 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
2024-02-29T00:18:10.273+09:00 INFO 14580 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 21 ms. Found 0 JPA repository interfaces.
2024-02-29T00:18:10.725+09:00 INFO 14580 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2024-02-29T00:18:10.730+09:00 WARN 14580 --- [ main] c.zaxxer.hikari.util.DriverDataSource : Registered driver with driverClassName=oracle.jdbc.driver.OracleDriver was not found, trying direct instantiation.
2024-02-29T00:18:11.042+09:00 INFO 14580 --- [ main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection oracle.jdbc.driver.T4CConnection@113dcaf8
2024-02-29T00:18:11.045+09:00 INFO 14580 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
2024-02-29T00:18:11.087+09:00 INFO 14580 --- [ main] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [name: default]
2024-02-29T00:18:11.161+09:00 INFO 14580 --- [ main] org.hibernate.Version : HHH000412: Hibernate ORM core version 6.4.1.Final
2024-02-29T00:18:11.210+09:00 INFO 14580 --- [ main] o.h.c.internal.RegionFactoryInitiator : HHH000026: Second-level cache disabled
2024-02-29T00:18:11.495+09:00 INFO 14580 --- [ main] o.s.o.j.p.SpringPersistenceUnitInfo : No LoadTimeWeaver setup: ignoring JPA class transformer
2024-02-29T00:18:11.614+09:00 WARN 14580 --- [ main] org.hibernate.dialect.Dialect : HHH000511: The 11.2.0 version for [org.hibernate.dialect.OracleDialect] is no longer supported, hence certain features may not work properly. The minimum supported version is 19.0.0. Check the community dialects project for available legacy versions.
2024-02-29T00:18:11.972+09:00 INFO 14580 --- [ main] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
2024-02-29T00:18:11.977+09:00 INFO 14580 --- [ main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2024-02-29T00:18:12.541+09:00 WARN 14580 --- [ main] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
2024-02-29T00:18:13.052+09:00 INFO 14580 --- [ main] o.s.b.t.m.w.SpringBootMockServletContext : Initializing Spring TestDispatcherServlet ''
2024-02-29T00:18:13.052+09:00 INFO 14580 --- [ main] o.s.t.web.servlet.TestDispatcherServlet : Initializing Servlet ''
2024-02-29T00:18:13.054+09:00 INFO 14580 --- [ main] o.s.t.web.servlet.TestDispatcherServlet : Completed initialization in 2 ms
2024-02-29T00:18:13.076+09:00 INFO 14580 --- [ main] c.t.b.b.c.UserBenefitControllerUnitTest : Started UserBenefitControllerUnitTest in 3.899 seconds (process running for 4.86)
2024-02-29T00:18:13.528+09:00 INFO 14580 --- [ionShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2024-02-29T00:18:13.531+09:00 INFO 14580 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown initiated...
2024-02-29T00:18:13.537+09:00 INFO 14580 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown completed.
model 객체의 getAttribute() 메서드가 호출되어 benefitList 속성을 가져오지 못해서 오류가 발생했는데, 이를 해결하기 위해 다음의 두 가지 가능성을 고려해 보았다.
1. model 객체에 benefitList 속성을 적절히 설정하지 않았거나, 설정된 속성이 null일 수 있다.
2. 테스트 메서드에서 model 객체의 getAttribute() 메서드를 호출할 때 null을 반환하도록 목(Mock) 객체를 설정하지 않았을 수 있다.
이러한 가능성으로 미루어 model 객체의 getAttribute() 메서드가 호출될 때 올바른 값을 반환하도록 목(Mock) 객체를 설정하였다. 그리고 Mockito의 when() 메서드를 사용하여 model.getAttribute("benefitList")가 호출될 때 반환되는 값을 설정하였다.
설정 이후에 테스트 메서드가 model 객체의 getAttribute() 메서드를 호출할 때 목(Mock) 객체가 설정한 값을 반환하였고 오류가 해결되었다.
테스트 결과 분석
1. 테스트는 총 0.042초(42밀리초) 소요되었다.
2. testView() 메서드에서 JUnit의 assertAll()을 사용하여 여러 개의 단언문을 모두 확인하였고, 테스트는 성공적으로 실행되었다.
3. view() 메서드는 예상대로 호출되었고, 반환된 뷰 이름은 "user/benefit/view"와 일치한다.
4. 모델에 추가된 "benefitList" 속성은 예상 혜택 목록과 일치한다. 이를 통해 컨트롤러가 서비스로부터 올바른 혜택 목록을 가져오고, 이를 모델에 제대로 추가했음을 확인할 수 있다.
참고 자료
[Spring] JUnit과 Mockito 기반의 Spring 단위 테스트 코드 작성법, MangKyu's Diary, 2021.04.20.
[JUnit] 란 무엇일까?, 최동근, 2022.12.16.
Given When Then, Martin Fowler, 2013.08.21.
Testing in Spring Boot, baeldung, 2024.01.08.