1. 단위 테스트
단위 테스트는 소프트웨어의 가장 작은 부분인 개별 함수, 메소드, 클래스 같은 가장 작은 단위인 모듈 단위로 테스트를 하는 기법이다. 단위 테스트는 특정 코드가 잘 동작하는지 확인하기 위해 모듈 각각 개별 단위로 테스트 하며 다른 모듈이나 시스템과의 상호작용 없이 테스트 한다.
java에서는 쉽게 테스트할 수 있도록 JUnit5라는 테스트용 프레임워크를 제공한다.
testImplementation 'org.springframework.boot:spring-boot-starter-test'
junit5 프레임워크를 사용하려면 다음과 같은 의존성 추가가 필요하다.
소스 코드의 테스트하고자 하는 클래스를 오른쪽 마우스클릭을 하고 Generate -> Test를 누르면 사진과 같은 테스트 생성 화면이 나온다.
여기서 ok를 누르면 junit5와 관련된 라이브러리가 import된 테스트 소스코드가 생성된다.
junit5에는 테스트에 도움을 주는 여러가지 기능이 있다.
@BeforeEach: 각각의 테스트 코드가 실행되기 전에 실행된다.
void setUp() {
System.out.println("각각의 테스트 코드가 실행되기 전에 수행");
}
@AfterEach: 각각의 테스트 코드가 실행된 후에 실행된다.
@AfterEach
void tearDown() {
System.out.println("각각의 테스트 코드가 실행된 후에 최초로 수행\n");
}
@BeforeAll: 모든 테스트 코드가 수행되기 전에 한번만 실행된다. static메소드에만 붙일 수 있다.
@BeforeAll
static void beforeAll(){
System.out.println("모든 테스트 코드가 실행되기 전에 최초로 수행\n");
}
@AfterAll: 모든 테스트 코드가 수행된 후 마지막으로 한번만 실행된다. static메소드에만 붙일 수 있다.
@AfterAll
static void afterAll() {
System.out.println("모든 테스트 코드가 수행된 후 마지막으로 수행");
}
@DisplayName: 테스트에 이름을 붙여줄 수 있다.
@Test
@DisplayName("테스트의 내용을 한눈에 알아볼 수 있게 네이밍 해줄 때")
void test1() {
System.out.println("테스트 내용 빠르게 파악");
}
@Nested: 테스트 별로 그룹을 만들어서 관리할 수 있다.
@Nested
@DisplayName("주제 별로 테스트를 그룹지어서 파악하기 좋습니다.")
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class Test1 {
@Order(1)
@Test
@DisplayName("Test1 클래스")
void test() {
System.out.println("\nTest1 클래스");
}
@Order(3)
@Test
@DisplayName("Test1 - test1()")
void test1() {
System.out.println("Test1.test1");
}
@Order(2)
@Test
@DisplayName("Test1 - test2()")
void test2() {
System.out.println("Test1.test2");
}
}
@Order: 테스트 메소드의 실행 순서를 정할 수 있다. 낮은 수부터 실행된다. @Order를 쓰려면 @TestMethodOrder(MethodOrderer.OrderAnnotation.class) 어노테이션을 붙여줘야 한다.
Assertions:
Assertions는 테스트를 진행할 때 개발자가 의도한 결과를 리턴했는지 확인하는 클래스이다.
다음은 Assertions가 제공하는 메소드들 중 일부이다.
Assertions.assertEquals(expected, actual):
assertEquals() 메소드는 첫 번째 파라미터에 예상하는 값, 두 번째 파라미터에 테스트 결과 값을 넣어서 둘이 일치하는지 비교하는 메소드이다.
@Test
@DisplayName("assertEquals")
void test1() {
Double result = calculator.operate(5, "/", 2);
assertEquals(2.5, result);
}
@Test
@Disabled
@DisplayName("assertEquals - Supplier")
void test1_1() {
Double result = calculator.operate(5, "/", 0);
// 테스트 실패 시 메시지 출력 (new Supplier<String>())
assertEquals(2.5, result, () -> "연산자 혹은 분모가 0이 아닌지 확인해보세요!");
}
세 번째 파라미터로 String을 리턴하는 람다식을 넣으면 두 파라미터가 서로 다를때 메세지를 출력하게 할 수 있다.
Assertions.assertTrue(boolean), assertTrue(boolean):
assertTrue() 메소드는 해당 파라미터 값이 true인지 확인한다. assertFalse()는 false인지 확인한다.
@Test
@DisplayName("assertTrue 와 assertFalse")
void test2() {
assertTrue(calculator.validateNum(9));
assertFalse(calculator.validateNum(0));
}
Assertions.assertNull(actual), assertNotNull(actual) :
assertNull() 메서드는 해당 파라미터 값이 null임을 확인한다. assertNotNull()은 null이 아님을 확인한다.
@Test
@DisplayName("assertNotNull 과 assertNull")
void test3() {
Double result1 = calculator.operate(5, "/", 2);
assertNotNull(result1);
Double result2 = calculator.operate(5, "/", 0);
assertNull(result2);
}
Assertions.assertThrows(expectedType, executable)
assertThrows() 메서드는 예상하는 예외처리 클래스가 실제로 throw한 예외처리 클래스와 일치하는지 확인하는 메소드이다. 첫 번째 파라미터에 예상하는 Exception 클래스 타입을 넣고 두 번째 파라미터에 실행 코드를 넣으면 된다.
@Test
@DisplayName("assertThrows")
void test4() {
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> calculator.operate(5, "?", 2));
assertEquals("잘못된 연산자입니다.", exception.getMessage());
2. Given - When - Then
Given-When-Then 패턴은 Test Code 스타일을 표현하는 방식을 의미한다.
Given에서는 테스트 하고자하는 대상을 실행하기 전에 테스트에 필요한 값을 미리 선언해 둔다.
When에서는 테스트 하고자하는 대상을 실제로 실행 시킨다.
Then에서는 어떤 특정한 행동 때문에 발생할거라 예상되는 결과에 대해 예측하고 맞는지 확인한다.
예시:
@Test
@DisplayName("계산기 연산 성공 테스트")
void test1() {
// given
int num1 = 5;
String op = "/";
int num2 = 2;
// when
Double result = calculator.operate(num1, op, num2);
// then
assertNotNull(result);
assertEquals(2.5, result);
}
3. Mockito
단위 테스트를 위해 테스트 코드를 작성하다 보면 어떤 모듈은 다른 모듈들과 연결되어 있어 분리하기가 곤란한 상황이 있다. 그럴 때는 연결된 다른 모듈을 가짜 객체(Mock 객체)로 만들어서 테스트를 하면 된다.
Mockito는 테스트에 필요한 Mock객체를 쉽게 만들어주는 프레임워크이다.
@ExtendWith(MockitoExtension.class) // @Mock 사용을 위해 설정합니다.
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());
}
Mock객체로 선언할 클래스에 @Mock 어노테이션을 붙이면 아무 기능이 없는 빈 객체를 주입한다.
Mock객체에 속한 메소드를 테스트 코드에서 실행하는 것은 불가능하다. 대신 메소드가 실행되면 나오는 결과를 미리 '가정'해 둘 수는 있다. given-will 코드로 Mock객체의 메소드를 실행하면 어떤 값을 반환할 지 정해줄 수 있다.
given(mock객체의 메소드).willReturn(리턴값);
4. 통합테스트
통합 테스트는 여려 개의 모듈을 결합하여 전체 시스템이 예상대로 작동하는지 테스트하는 기법이다. 통합 테스트는 단위 테스트와는 달리 여러 모듈을 결합해서 모듈 간 상호작용에 문제가 없는지 테스트를 하고 단위 테스트와는 달리 다른 모듈 이나 시스템과 상호작용이 필요하다.
controller테스트를 하는 통합 테스트코드 예시:
@WebMvcTest(
controllers = {UserController.class, ProductController.class},
excludeFilters = {
@ComponentScan.Filter(
type = FilterType.ASSIGNABLE_TYPE,
classes = WebSecurityConfig.class
)
}
)
class UserProductMvcTest {
private MockMvc mvc;
private Principal mockPrincipal;
@Autowired
private WebApplicationContext context;
@Autowired
private ObjectMapper objectMapper;
@MockBean
UserService userService;
@MockBean
KakaoService kakaoService;
@MockBean
ProductService productService;
@MockBean
FolderService folderService;
@BeforeEach
public void setup() {
mvc = MockMvcBuilders.webAppContextSetup(context)
.apply(springSecurity(new MockSpringSecurityFilter()))
.build();
}
실제 애플리케이션에는 spring security 때문에 인증된 jwt토큰이 없으면 접근이 거부된다. 따라서 security관련 config 클래스를 테스트 코드에서는 사용하지 못하게 excludeFilters 속성으로 제외를 시켰다.
대신 가짜 security필터를 만들어서 security가 설정 되어 있는 것처럼 세팅을 했다.
private void mockUserSetup() {
// Mock 테스트 유져 생성
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());
}
컨트롤러의 api를 테스트 하기 위해 mvc.perform()메소드를 사용했다. perform()의 파라미터로 api의 메소드타입과 uri를 입력한다 (method("uri")). parms()메소드로 api를 요청할 때 같이 보낼 파라미터를 지정할 수 있다.
예상 실행 결과는 andExpect()메소드로 표현한다. 여기서는 statuscode와 반환된 body의 값을 확인했다.
마지막에 andDo()로 실행 결과를 콘솔에 출력하는 기능을 넣었다.
'내일배움캠프' 카테고리의 다른 글
아웃소싱 프로젝트 1일차 (0) | 2024.06.19 |
---|---|
Spring 심화주차 Part3 (0) | 2024.06.19 |
gradle의 clean기능 (0) | 2024.06.13 |
Spring 심화주차 Part1 (0) | 2024.06.13 |
뉴스피드 프로젝트 5일차 (0) | 2024.06.11 |