코드가 Testable 하게 짜여 있지가 않아서 어떻게 테스트를 할지 잘 모르기 때문이다!
Test - SOLID 원칙은 매우 밀접한 관계이다.
Full Coverage까진 못 가더라도, 보통 성공 케이스와 주요한 실패 케이스는 고려해서 작성하는 게 좋다. 안정성을 확보하는 데에 아주 중요한 역할을 한다!
작은 단위로 쪼개서 각 단위가 정확하게 동작하는지를 검사
Edge 포인트를 주면서 메소드 하나 내에서도 여러 개의 테스트 코드를 수행해볼 수 있다
자바 프로그래밍 언어 용 단위 테스트 프레임워크 : JUnit5
@BeforeEach
void setUp() { }@AfterEach
void tearDown() { } @BeforeAll
static void beforeAll() { }
@AfterAll
static void afterAll() { }
@DisplayName("테스트 내용 알아보기 쉽게 네이밍")@Nested : 주제 별로 테스트를 그룹 짓기@Order(순서) : 테스트 순서 지정@RepeatedTest(value = 5, name = "반복 테스트 {currentRepetition} / {totalRepetitions}")
void repeatTest(RepetitionInfo info) {
System.out.println("테스트 반복 : " + info.getCurrentRepetition() + " / " + info.getTotalRepetitions());
}
value = 총 테스트 돌려볼 횟수
{currentRepetition} / {totalRepetitions} : 현재 반복 횟수 / 총 횟수
@DisplayName("파라미터 값 활용하여 테스트 하기")
@ParameterizedTest
@ValueSource(ints = {1, 2, 3, 4, 5, 6, 7, 8, 9})
void parameterTest(int num) {
System.out.println("5 * num = " + 5 * num);
}
@ValueSource : 전달할 파라미터 값, 갯수 만큼 테스트한다
assertEquals(기댓값, 결과값 <을 담은 변수>) : 두 값이 일치하지 않을 시 오류
assertEquals(기댓값, 결과값 <을 담은 변수>, () -> "오류 시 던질 메시지")
assertNotEquals(참일 때 기댓값, 결과값 <을 담은 변수>) : 두 값이 일치할 시 오류
assertTrue(calculator.validateNum(9)); : return 값이 True인지 확인
assertFalse(calculator.validateNum(0)); : return 값이 False인지 확인
assertNotNull()
assertNull()
@Test
@DisplayName("assertThrows")
void test4() {
// 던진 오류를 담아서 변수에 담기
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> calculator.operate(5, "?", 2));
// 오류의 메시지가 예측값과 동일한지 확인
assertEquals("잘못된 연산자입니다.", exception.getMessage());
}
지정한 Exception 발생 되는지 확인한다.
given : 이런 상황 시 (미리 준비한 테이터)
when : 이런 호출을 하면 (함수 실행)
then : 이렇게 된다. (assert)
실패 시 예외 처리가 성공했는지도 확인 후 던져야 한다.
실행과 결과가 같이 나올 경우 when - then 으로 작성하기도 한다.
1) Test 클래스명 위에 @ExtendWith(MockitoExtension.class)
2) @Mock 을 사용해서 Repository를 받아온다.
이때 하고 싶은 테스트가 update / save 등이고 repository에서 읽어오는 부분이 아니라면, repository에서 읽어오는 부분은 우리가 따로 명시해줘야 한다.
@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를 만들고, RequestDto에 값을 삽입 (빈 생성자가 아님)하는 아래의 과정을 직접 명시해줘야 한다.
// when 전까지만 다르고, 나머지 부분은 동일
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());
}
Product 객체를 만드는 부분은 우리가 테스트하고 싶은 부분이 아니기 때문에 직접 명시해준다.
이때 MyPrice는
given(값을 넣어줄 부분 코드.리턴 받아올 값)
즉 위의 코드에서는 productRepository.findById(productId) 수행 시 Optional.of(product)를 리턴하게 되는 것이다.

단위 테스트 > 통합 테스트 > UI 테스트
이때 단위 테스트의 비중이 80%에 가깝다!
@SpringBootTest 사용 시 스프링이 동작한다. (단위 테스트 시 동작 X)
즉, Spring IoC/DI 기능을 사용 가능 / Repository를 사용해 DB CRUD가 가능합니다.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) // 서버의 PORT 를 랜덤으로 설정합니다.
@TestInstance(TestInstance.Lifecycle.PER_CLASS) // 테스트 인스턴스의 생성 단위를 클래스로 변경합니다.
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@TestInstance 사용 전에는 각각의 메소드가 따로 수행되며, 서로 영향을 끼치지 않는다. 전역 변수를 공유하지 않는다.
@TestMethodOrder : @Order을 사용하기 위해서 추가한다.
@SpringBootTest 사용 시 @AutoWired를 사용해서 Bean으로 주입 받아올 수 있다.
테스트를 통해 생성된 객체와, repository에서 찾아온 객체가 같은지 비교한다 (저장이 잘 되었는지 확인)
Mockup Filter 만들기
@WebMvcTest : 테스트 클래스 외부에서 테스트 할 컨트롤러를 지정, 제외할 필터를 지정@WebMvcTest(
controllers = {UserController.class, ProductController.class},
excludeFilters = {
@ComponentScan.Filter(
type = FilterType.ASSIGNABLE_TYPE,
classes = WebSecurityConfig.class
)
}
)
Mock 테스트를 위한 유저가 필요하다
@Test
@DisplayName("로그인 Page")
void test1() throws Exception {
// when - then
mvc.perform(get("/api/user/login-page")) // get 방식 URI
.andExpect(status().isOk()) // 예상되는 상태 값
.andExpect(view().name("login")) // 예상되는 view의 이름을 login으로 지정하기
.andDo(print()); // 출력하기
}
해당 컨트롤러가 URL을 잘 반환하는지 확인하는 역할이다.
@EnableJpaAuditing 때문에 오류 발생할 수 있다.main 앞에 붙어있는 @EnableJpaAuditing을 삭제 후, 따로 Config 클래스 만들어서 거기에 달아 준다.
+) 해당 설정 활성화를 위해 @Configuraion 달아 준다.
ModelAndView ?
@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)
// 위에서 만든 requestForm을 post에서 입력 받는다.
)
.andExpect(status().is3xxRedirection()) // redirect 타입이라 3으로 시작한다.
.andExpect(view().name("redirect:/api/user/login-page")) // 반환할 url
.andDo(print());
}
requestForm.add에 key - value 형식으로 값을 입력한다.
@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) // requestDto 타입
.accept(MediaType.APPLICATION_JSON)
.principal(mockPrincipal) // 위에서 받아온 mock 인증
)
.andExpect(status().isOk())
.andDo(print());
}
requestDto 값을 지정해서 request 값을 준다.