테스트시 이점
- 테스트를 짜면서 코드를 자연스럽게 리뷰하게 됩니다.
- 테스트 하기 어려운 경우, 코드가 잘못된 것을 빨리 알아챌 수 있습니다.
- 너무 다양한 일을 하고 역할이 많은 경우 테스트 하기 어렵습니다.
- 테스트가 잘 되어 있으면 리팩토링이 수월합니다.
- 코드의 품질이 좋아집니다.
- 현재 코드의 세부적인 정책들이 모두 테스트로 문서화 됩니다.
- 장기적으로 볼 때 더 빠르고 안정적인 개발이 가능해집니다.
테스트 작성 유의사항
- 클래스나 메소드가 SRP를 잘 지키고 너무 크지 않도록 합니다.
- 유닛 테스트의 경우 적절한 Mocking으로 격리성을 확보합니다.
- 테스트 커버리지를 높여서 테스트가 안되는 부분이 없도록 합니다.
- 테스트 코드도 코드라는 생각으로 개선합니다.
테스트 도구
JUnit
- xUnit이라는 유닛테스트 프레임워크를 기반으로 Java용으로 개발된 프레임워크
- 단위 테스트를 실행하고 결과를 거증해서 전체 결과를 리포트해줍니다.
- 사용자가 직접 동작시킬 수 있으며 Gradle이나 Maven을 통해 빌드하면서 테스트 가능합니다.
- spring-boot-starter-test에 기본적으로 Junit5가 포함됩니다.
- 예시
- @SpringBootTest
- 스프링 부트에서 통합테스트 시 사용하는 어노테이션
- 어플리케이션 내부의 모든 빈을 등록해줍니다.
- webEnvironment.Mock이 기본값으로 설정되어있기에 서블릿 컨테이너가 모킹됩니다.
- 단위테스트 시에는 사용하지 않습니다.
- 스프링 부트에서 통합테스트 시 사용하는 어노테이션
- @BeforeEach
- 각 테스트를 실행하기 전에 수행되는 매번 실행되는 메소드
- 모든 테스트를 실행하기 전에 딱 한번만 실행하고자 하면 메소드를 static으로 선언하고 @Before 대신 @BeforeAll을 사용합니다.
- 반대로 동작하는 @After~ 도 있습니다.
- @Test
- 테스트를 만드는 모듈역할입니다.
- @Test가 붙어있는 메소드는 IDE로 직접 실행할 수 있습니다.
- @DisplayName
- 테스트 명을 지정할 수 있습니다.
- assertEquals()
- 두 객체의 값이 동일한지를 비교하기 위한 메소드입니다.
- @SpringBootTest
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class AccountServiceTest {
@Autowired
private AccountService accountService;
// 각 테스트 초기화 시에 실행
@BeforeEach
void init() {
accountService.createAccount();
}
@Test
@DisplayName("Test 이름 변경") // 테스트명 변경
void testGetAccount() {
Account account = accountService.getAccount(1L);
assertEquals("4", account.getAccountNumber());
assertEquals(AccountStatus.IN_USE, account.getAccountStatus());
}
}
- Junit만 사용하는 경우 문제점
- 테스트하고자 하는 클래스가 의존하는 클래스를 모두 만들어야하다보니 테스트를 만들기가 어렵습니다.
- 모든 클래스가 동작하다보면 어떤 부분이 문제인지 알기가 어렵습니다.
Mockito
- Mock을 대신 만들어주는 라이브러리.
- Mock 객체를 직접 구현하지 않아도 자동으로 만들어 주기 때문에 시간을 절약해 줍니다.
- Mock을 이용하기 때문에 테스트 별로 격리성이 뛰어납니다.
- 예시
- Junit 예시를 Mockito방식으로 변경
- 설명
- @RunWith(MockitoExtension.class)
- Mockito에서 제공하는 Mock 객체를 사용하기 위해서 테스트 클래스에 부착합니다.
- @Mock
- Mock 객체를 생성합니다.
- @InjectMocks
- @Mock이 붙은 Mock 객체를 @InjectMocks가 붙은 객체에 주입시킵니다 (= Autowired)
- 실무에서는 @InjectMocks(Service) @Mock(Repository) 이런 식으로 사용합니다.
- 만약 테스트할 객체내의 메소드가 다른
- @RunWith(MockitoExtension.class)
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.BDDMockito.given;
@ExtendWith(MockitoExtension.class)
class AccountServiceTest {
@Mock
private AccountRepository accountRepository;
@InjectMocks
private AccountService accountService;
@Test
@DisplayName("계좌 조회 성공")
void testXXX(){
//given
given(accountRepository.findById(anyLong()))
.willReturn(Optional.of(Account.builder()
.accountStatus(AccountStatus.UNREGISTERED)
.accountNumber("12345")
.build()));
//when
Account account = accountService.getAccount(1234L);
//then
assertEquals("12345",account.getAccountNumber());
assertEquals(AccountStatus.UNREGISTERED,account.getAccountStatus());
}
}
Controller 테스트 방법
Mock
- 테스트를 위해 만든 모형
- Mocking = Mock을 만드는 것
- Mock-up = Mocking 한 객체를 메모리에서 얻어내는 과정
- 테스트 하려는 객체가 복잡한 의존성을 가지고 있을 때, 모킹한 객체를 이용하면 의존성을 단절시킬 수 있어 쉽게 테스트 가능합니다.
MockMVC
- 스프링 MVC 테스트 유틸리티 클래스 입니다.
- 서블릿 컨테이너를 직접 실행하지 않고 모킹해서 HTTP 요청과 응답을 생성해 줍니다.
- Postman과 같은 도구를 사용하지 않고도 테스트가 가능해집니다.
- 서블릿 컨테이너를 모킹한 객체입니다.
- Controller를 호출해 주는 도구라고 생각하면 됩니다.
- MockMvc를 주입받아 사용할 수 있게 해줍니다.
MockMVC 메소드
- perform()
- 브라우저에서 서버에 URL 요청을 하듯 컨트롤러를 실행시킬 수 있습니다.
- RequestBuilder 객체를 인자로 받는데, 이는 MockMvcRequestBuilder의 정적메소드를 이용해서 생성합니다.
- ResultActions 객체를 반환합니다.
MockMvcRequestBuilders
- get() , post() , put() , delete() 메소드를 제공합니다.
- MockHttpServletRequestBuilder 객체를 리턴합니다.
MockHttpServletRequestBuilder
- 이 객체를 통해 HTTP 요청 관련 정보(파라미터, 헤더, 쿠키 등)를 설정할 수 있습니다.
- 메시지 체인을 구성하여 복잡한 요청을 설정할 수 있습니다.
ResultActions
- andExpect()를 통해 응답결과를 검증할 수 있습니다.
- 이 메소드는 인자로 ResultMatcher를 요구하는데, MockMvcResultMatchers의 정적 메소드를 통해 생성할 수 있습니다.
MockMvcResultMatcher
- 응답 상태 코드를 검증해줍니다.
isOk() | 응답 상태 코드가 정상적인 처리(200)인지 확인 |
isNotFount() | 응답 상태 코드가 404 Not Found인지 확인 |
isMethodNotAllowed() | 응답 상태 코드가 메소드 불일치(405)인지 확인 |
isInternalServerError() | 응답 상태 코드가 예외발생(500)인지 확인 |
is(int status) | 몇 번 응답 상태 코드가 설정되었는지 확인 ex) is(200), is(404) |
@SpringBootTest + @AutoConfigureMockMvc
- 특징
- 프로젝트 내부에 있는 스프링 빈을 모두 등록하여 테스트에 필요한 의존성을 추가합니다.
- 실제 운영 환경에서 사용될 클래스들을 통합하여 테스트 합니다.
- 단위 테스트 같이 기능 검증을 위한 것이 아닌 전체적인 Flow가 제대로 동작하는지 검증하기 위해 사용합니다.
- MockMvc를 보다 세밀하게 제어하기 위해서 사용합니다.
- 장점
- 운영환경과 가장 유사한 테스트가 가능합니다.
- 단점
- 테스트 단위가 크기 때문에 디버깅이 까다롭습니다.
- 모든 설정과 빈을 로드하기 때문에 시간이 오래걸립니다.
- @AutoConfigureMockMvc
- @SpringBootTest에서도 Mock 테스트를 가능하게 해주는 @WebMVcTest와 비슷한 역할 입니다.
- 다만, Controller 뿐만 아니라 다른 컴포넌트들도 메모리에 올립니다.
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.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest
@AutoConfigureMockMvc
class AccountControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
AccountService accountService;
@Test
void successGetAccount() throws Exception {
//given
given(accountService.getAccount(anyLong()))
.willReturn(Account.builder()
.accountNumber("12345")
.accountStatus(AccountStatus.IN_USE)
.build());
//when
//then
mockMvc.perform(get("/account/1234")) // url로 GET 요청을 전송합니다.
.andDo(print()) // 응답 값을 화면에 표시해 줍니다.
.andExpect(jsonPath("$.accountNumber").value("12345")) // 결과를 검증합니다.
.andExpect(jsonPath("$.accountStatus").value("IN_USE"))
.andExpect(status().isOk()); // 응답코드를 검증합니다.
}
}
@WebMvcTest
- 특징
- 내가 필요로 하는 MVC 관련 Bean 들만 생성
- Web Layer만 스캔합니다.
- Controller , ControllerAdvice, Converter , Filter, HandlerInterceptor 등
- @Component는 스캔대상에서 제외됩니다.
- Service 등 Controller에서 의존하는 하위 레이어의 기능은 Mockito가 아닌 스프링에서 지원하는 @MockBean을 통해 모킹해서 원하는 동작을 하도록 합니다.
- 테스트 하려는 객체가 복잡한 의존성을 가지고 있을 때, 모킹한 객체를 이용하면 의존성을 단절시킬 수 있어 쉽게 테스트 가능합니다.
- 단위 테스트가 가능합니다.
- Web Layer만 스캔합니다.
- 내가 필요로 하는 MVC 관련 Bean 들만 생성
- 장점
- 필요한 빈만 등록하기 때문에 @SpringBootTest보다 빠릅니다.
- 통합테스트를 진행하기 어려운 테스트를 개별적으로 진행 가능합니다.
- 단점
- Mock을 기반으로 테스트하기 때문에 , 실제 환경에서는 오류가 발생할 수 있습니다.
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
// 테스트하기위한 컨트롤러를 명시하여 Bean 등록하고 MockMvc를 주입해줍니다.
// 컨트롤러 명시를 생략할 경우, 모든 컨트롤러 Bean이 등록됩니다.
@WebMvcTest(AccountController.class)
class AccountControllerTest {
@Autowired
private MockMvc mockMvc;
// 목객체를 빈으로 등록해줍니다. 이 목 빈은 컨트롤러에 DI됩니다.
@MockBean
private AccountService accountService;
@MockBean
private RedisTestService redisTestService;
@Test
void successGetAccount() throws Exception {
//given
given(accountService.getAccount(anyLong()))
.willReturn(Account.builder()
.accountNumber("12345")
.accountStatus(AccountStatus.IN_USE)
.build());
//when
//then
mockMvc.perform(get("/account/1234")) // url로 GET 요청을 전송합니다.
.andDo(print()) // 응답 값을 화면에 표시해 줍니다.
.andExpect(jsonPath("$.accountNumber").value("12345")) // 결과를 검증합니다.
.andExpect(jsonPath("$.accountStatus").value("IN_USE"))
.andExpect(status().isOk()); // 응답코드를 검증합니다.
}
}
- @SpringBootTest + @AutoConfigureMockMvc의 경우, 테스트 하고자하는 빈만 목으로 만들어주면 다른 빈은 전부 자동으로 등록되기 때문에 테스트하고자 하는 서비스만 코드작성하면 되는데, @WebMvcTest의 경우 컨트롤러 안에 있는 모든 빈을 목으로 만들어주어야 한다.(안하면 예외발생함)
Service 테스트 방법
- verify
- 의존하고 있는 Mock이 해당되는 동작을 수행했는지 확인하는 검증합니다.
- 자세한 내용은 https://velog.io/@dnjscksdn98/JUnit-Mockito-Verify-Method-Calls
verify(accountRepository, times(1)).save(any<Account>());
verify(accountRepository, times(0)).findById(anyLong());
- ArgumentCaptor
- 의존하고 있는 Mock에 전달된 데이터가 내가 의도하는 데이터가 맞는지 검증
ArgumentCaptor<Account> captor = ArgumentCaptor.forClass(Account.class);
verify(accountRepository, times(1)).save(captor.capture());
assertEquals("1234", captor.getValue().getAccountNumber());
- assertions
- 다양한 비교 방법들
assertEquals("1234", captor.getValue().getAccountNumber());
assertNotEquals("1234", captor.getValue().getAccountNumber());
assertNull(result);
assertNotNull(result);
assertTrue(resut.getBoolean());
assertFalse(resut.getBoolean());
assertAll(
() -> assertTrue(resut.getBoolean()),
() -> assertFalse(resut.getBoolean())
);
- assertThrows
- 예외를 던지는 로직을 테스트
AccountException exception =
assertThrows(AccountException.class, () -> accountService.getAccount(123L));
assertEquals(ACCOUNT_NOT_FOUND, exception.getErrorCode());
'Spring Framework > TDD' 카테고리의 다른 글
단위 테스트 (Unit Test) (0) | 2022.06.21 |
---|---|
Mockito (0) | 2022.06.21 |
JpaAuditing이 Test할 때 적용이 안되는 경우 (0) | 2022.06.12 |
댓글