[수업 목표]
1. 테스트의 필요성을 이해한다.
2. 스프링 테스트 프레임워크 이해 및 사용 학습한다.
출처: 위키백과
소프트웨어 내부 구조나 동작원리를 모르는 블랙박스와 같은 상태에서, 즉 웹 서비스의 사용자 입장에서 동작을 검사하는 방법
개발자가 직접 "본인이 작성한 코드"를 검증하기 위해 "테스트 코드"를 작성
스프링에서 '테스트 코드' 작성을 잘 할 수 있는 환경 제공 해줌!
프로그램을 작은 단위로 쪼개서 각 단위가 정확하게 동작하는지 검사하고 이를 통해 문제 발생 시 정확하게 어느 부분이 잘못되었는 지를 재빨리 확인할 수 있게 해줌
출처: 단위 테스트(위키백과)
버그 발견 시간이 늦어짐에 따라 비용이 기하급수적으로 커짐
JUnit이란 자바 프로그래밍 언어용 단위 테스트 프레임워크
dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
test {
useJUnitPlatform()
}
import com.sparta.springcore.dto.ProductRequestDto;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class ProductTest {
// JUnit에서 제공하는 것, JUnit으로 테스트를 돌려주겠다
// (함수 위에 붙임)
@Test
// (없으면 Run할 때 함수 이름 표시됨)
@DisplayName("정상 케이스")
void createProduct_Normal() {
// given (이런 환경이 주어졌을 때)
Long userId = 100L;
String title = "오리온 꼬북칩 초코츄러스맛 160g";
String image = "https://shopping-phinf.pstatic.net/main_2416122/24161228524.20200915151118.jpg";
String link = "https://search.shopping.naver.com/gate.nhn?id=24161228524";
int lprice = 2350;
ProductRequestDto requestDto = new ProductRequestDto(
title,
image,
link,
lprice
);
// when (이 코드를 실행하면 -> 이 테스트를 하기 위한 것임!)
// [생성자 코드 검증]
Product product = new Product(requestDto, userId);
// then (결과로 이렇게 되어야한다)
// 인자값이 null이 된다고 강하게 주장, Null 아니면 에러
assertNull(product.getId());
// 같다고 강하게 주장
// (expected[테스트하고 되었으면 좋겠다고 기대한 값],
// actual[실제 테스트하고 나서 그 결과를 넣는 것])
assertEquals(userId, product.getUserId());
assertEquals(title, product.getTitle());
assertEquals(image, product.getImage());
assertEquals(link, product.getLink());
assertEquals(lprice, product.getLprice());
assertEquals(0, product.getMyprice());
}
}
테스트 작성 시 Edge케이스를 고려하는 것이 굉장히 중요함
// 회원 Id
Long userId = 1230L;
// 상품명
String title = "오리온 꼬북칩 초코츄러스맛 160g";
// 상품 이미지 URL
String image = "https://shopping-phinf.pstatic.net/main_2416122/24161228524.20200915151118.jpg";
// 상품 최저가 페이지 URL
String link = "https://search.shopping.naver.com/gate.nhn?id=24161228524";
// 상품 최저가
int lprice = 2350;
회원 Id
1) 회원 아이디 (userId)가 null 로 들어오면, 등록된 상품은 어떤 회원의 상품이 되는 거지?
2) 회원 아이디(userId)가 마이너스 값이면, 등록된 상품은 어떤 회원의 상품이 되는 거지?
(DB 테이블 Id의 경우 마이너스 값을 가질 수 없음)
상품명
1) 상품명이 null로 들어오면?
2) 상품명이 빈 문자열인 경우도 저장해야 할까?
-> 저장하면 UI에서는 어떻게 표시?
상품 이미지 URL
1) 상품 이미지 URL이 null로 들어오면?
2) 상품 이미지 URL이 URL 형태가 아니면?
-> UI에는 어떻게 표시?
상품 최저가 페이지 URL
1) 상품 최저가 URL이 null로 들어오면?
2) 상품 최저가 페이지 URL이 URL 형태가 아니면?
-> UI에는 어떻게 동작?
상품 최저가
1) 상품 최저가가 0이면? (공짜 상품?)
2) 상품 푀저가가 음수?
Edge 케이스에 대해 개발자가 독단적으로 방향을 결정하지 않고, 관련 담당자(들)과 협의 진행 후 결정
ex)
(ex)
if (userId == null || userId <= 0) {
throw new IllegalArgumentException("회원 Id 가 유효하지 않습니다.");
}
(ex)
boolean isValidUrl(String url)
{
try {
new URL(url).toURI();
return true;
}
catch (URISyntaxException exception) {
return false;
}
catch (MalformedURLException exception) {
return false;
}
}
import com.sparta.springcore.dto.ProductRequestDto;
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
class ProductTest {
@Nested // Depth로 트리 처럼 내려감
@DisplayName("회원이 요청한 관심상품 객체 생성")
class CreateUserProduct {
private Long userId;
private String title;
private String image;
private String link;
private int lprice;
@BeforeEach // 테스트 수행 전에 이 부분을 한 번씩 수행한다!
void setup() {
userId = 100L;
title = "오리온 꼬북칩 초코츄러스맛 160g";
image = "https://shopping-phinf.pstatic.net/main_2416122/24161228524.20200915151118.jpg";
link = "https://search.shopping.naver.com/gate.nhn?id=24161228524";
lprice = 2350;
}
@Test
@DisplayName("정상 케이스")
void createProduct_Normal() {
// given
ProductRequestDto requestDto = new ProductRequestDto(
title,
image,
link,
lprice
);
// when
Product product = new Product(requestDto, userId);
// then
assertNull(product.getId());
assertEquals(userId, product.getUserId());
assertEquals(title, product.getTitle());
assertEquals(image, product.getImage());
assertEquals(link, product.getLink());
assertEquals(lprice, product.getLprice());
assertEquals(0, product.getMyprice());
}
@Nested
@DisplayName("실패 케이스")
class FailCases {
@Nested
@DisplayName("회원 Id")
class userId {
@Test
@DisplayName("null")
void fail1() {
// given
userId = null;
ProductRequestDto requestDto = new ProductRequestDto(
title,
image,
link,
lprice
);
// when
// 에러가 throw 되어야한다(리턴값, Exception, error명도 같아야함)
Exception exception = assertThrows(IllegalArgumentException.class, () -> {
new Product(requestDto, userId);
});
// then
// 사실 메시지를 비교하는 것은 좋은 테스트 코드는 아님
assertEquals("회원 Id 가 유효하지 않습니다.", exception.getMessage());
}
@Test
@DisplayName("마이너스")
void fail2() {
// given
userId = -100L;
ProductRequestDto requestDto = new ProductRequestDto(
title,
image,
link,
lprice
);
// when
Exception exception = assertThrows(IllegalArgumentException.class, () -> {
new Product(requestDto, userId);
});
// then
assertEquals("회원 Id 가 유효하지 않습니다.", exception.getMessage());
}
}
@Nested
@DisplayName("상품명")
class Title {
@Test
@DisplayName("null")
void fail1() {
// given
title = null;
ProductRequestDto requestDto = new ProductRequestDto(
title,
image,
link,
lprice
);
// when
Exception exception = assertThrows(IllegalArgumentException.class, () -> {
new Product(requestDto, userId);
});
// then
assertEquals("저장할 수 있는 상품명이 없습니다.", exception.getMessage());
}
@Test
@DisplayName("빈 문자열")
void fail2() {
// given
String title = "";
ProductRequestDto requestDto = new ProductRequestDto(
title,
image,
link,
lprice
);
// when
Exception exception = assertThrows(IllegalArgumentException.class, () -> {
new Product(requestDto, userId);
});
// then
assertEquals("저장할 수 있는 상품명이 없습니다.", exception.getMessage());
}
}
@Nested
@DisplayName("상품 이미지 URL")
class Image {
@Test
@DisplayName("null")
void fail1() {
// given
image = null;
ProductRequestDto requestDto = new ProductRequestDto(
title,
image,
link,
lprice
);
// when
Exception exception = assertThrows(IllegalArgumentException.class, () -> {
new Product(requestDto, userId);
});
// then
assertEquals("상품 이미지 URL 포맷이 맞지 않습니다.", exception.getMessage());
}
@Test
@DisplayName("URL 포맷 형태가 맞지 않음")
void fail2() {
// given
image = "shopping-phinf.pstatic.net/main_2416122/24161228524.20200915151118.jpg";
ProductRequestDto requestDto = new ProductRequestDto(
title,
image,
link,
lprice
);
// when
Exception exception = assertThrows(IllegalArgumentException.class, () -> {
new Product(requestDto, userId);
});
// then
assertEquals("상품 이미지 URL 포맷이 맞지 않습니다.", exception.getMessage());
}
}
@Nested
@DisplayName("상품 최저가 페이지 URL")
class Link {
@Test
@DisplayName("null")
void fail1() {
// given
link = "https";
ProductRequestDto requestDto = new ProductRequestDto(
title,
image,
link,
lprice
);
// when
Exception exception = assertThrows(IllegalArgumentException.class, () -> {
new Product(requestDto, userId);
});
// then
assertEquals("상품 최저가 페이지 URL 포맷이 맞지 않습니다.", exception.getMessage());
}
@Test
@DisplayName("URL 포맷 형태가 맞지 않음")
void fail2() {
// given
link = "https";
ProductRequestDto requestDto = new ProductRequestDto(
title,
image,
link,
lprice
);
// when
Exception exception = assertThrows(IllegalArgumentException.class, () -> {
new Product(requestDto, userId);
});
// then
assertEquals("상품 최저가 페이지 URL 포맷이 맞지 않습니다.", exception.getMessage());
}
}
@Nested
@DisplayName("상품 최저가")
class LowPrice {
@Test
@DisplayName("0")
void fail1() {
// given
lprice = 0;
ProductRequestDto requestDto = new ProductRequestDto(
title,
image,
link,
lprice
);
// when
Exception exception = assertThrows(IllegalArgumentException.class, () -> {
new Product(requestDto, userId);
});
// then
assertEquals("상품 최저가가 0 이하입니다.", exception.getMessage());
}
@Test
@DisplayName("음수")
void fail2() {
// given
lprice = -1500;
ProductRequestDto requestDto = new ProductRequestDto(
title,
image,
link,
lprice
);
// when
Exception exception = assertThrows(IllegalArgumentException.class, () -> {
new Product(requestDto, userId);
});
// then
assertEquals("상품 최저가가 0 이하입니다.", exception.getMessage());
}
}
}
}
}
클래스를 가져다 쓰는 방법
-> @Component해서 빈으로 등록한 후 그 빈을 DI 받아서 사용하면됨
-> But! DI를 받을 수 있는 조건 : 스프링 Bean 끼리만 가능
(Product는 빈이 아니라서 못 함)
-> 따라서, static으로 함수를 선언하면 클래스 이름 써서 호출 가능!
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class URLValidatorTest {
@Test
@DisplayName("URL 형태: 정상")
void urlValidator1() {
// given
String url = "https://shopping-phinf.pstatic.net/main_8232398/82323985017.4.jpg";
// when
boolean isValid = URLValidator.isValidUrl(url);
// then
// url 정상일 때는 항상 true로 나와야 함
assertTrue(isValid);
}
@Test
@DisplayName("URL 형태: 비정상 (null 인 경우)")
void urlValidator2() {
// given
String url = null;
// when
boolean isValid = URLValidator.isValidUrl(url);
// then
assertFalse(isValid);
}
@Test
@DisplayName("URL 형태: 비정상 (빈 문자열)")
void urlValidator3() {
// given
String url = "";
// when
boolean isValid = URLValidator.isValidUrl(url);
// then
assertFalse(isValid);
}
@Test
@DisplayName("URL 형태: 비정상 (일반 문자열)")
void urlValidator4() {
// given
String url = "단위 테스트";
// when
boolean isValid = URLValidator.isValidUrl(url);
// then
assertFalse(isValid);
}
@Test
@DisplayName("URL 형태: 비정상 (`://` 빠짐)")
void urlValidator5() {
// given
String url = "httpfacebook.com";
// when
boolean isValid = URLValidator.isValidUrl(url);
// then
assertFalse(isValid);
}
}
int myprice = requestDto.getMyprice();
if (myprice < MIN_MY_PRICE) {
throw new IllegalArgumentException("유효하지 않은 관심 가격입니다. 최소 " + MIN_MY_PRICE + " 원 이상으로 설정해 주세요.");
}
이상적으로, 각 테스트 케이스는 서로 분리되어야 한다.
이를 위해 가짜 객체(Mock object)를 생성하는 것도 좋은 방법이다.
출처: 단위 테스트 (위키백과)
-> MockProductRepository, MockProductService 필요없음!
에러!!! Mock을 선언만 하고 사용 케이스를 제대로 정의하지 못했기 때문!
ProductRespository에 없어서 에러
-> Mock 객체에 대해 가짜함수를 호출해줄 수 있지만,
그 결과에 대해 명시해줘야함
// input 이거면 output 이거 내줘라!!!!
when(productRepository.findById(productId))
// -> 함수 호출했을때 (Mock 객체에 대해서만)
.thenReturn(Optional.of(product))
// -> 결과로 Optional product(샘플)를 만들어서 반환해라!
출처: Unit Testing: Creating Functional Alexa Skills
class ProductTest {
@Autowired
ProductService productService;
// ...
@Test
@DisplayName("정상 케이스")
void createProduct_Normal() {
// ...
// 에러 발생! productService 가 null
Product productByService = productService.createProduct(requestDto, userId);
import com.sparta.springcore.dto.ProductMypriceRequestDto;
import com.sparta.springcore.dto.ProductRequestDto;
import com.sparta.springcore.model.Product;
import com.sparta.springcore.service.ProductService;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
// @SpringBootTest 하면 스프링 뜰 때 포트 번호가 필요함(랜덤으로)
// (다른 애플리케이션 돌리고 있을 때 같은 포트로 돌리면 에러가 뜸)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
// @Order 쓰기 위해서 사용
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class ProductIntegrationTest {
@Autowired
ProductService productService;
Long userId = 100L;
Product 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 = 77000;
ProductRequestDto requestDto = new ProductRequestDto(
title,
imageUrl,
linkUrl,
lPrice
);
// when
Product product = productService.createProduct(requestDto, userId); // 실제로 DB에 생성함(단위 테스트와는 다르게), Product == DB에 만들어져 가져온 데이터
// then
assertNotNull(product.getId());
assertEquals(userId, product.getUserId());
assertEquals(title, product.getTitle());
assertEquals(imageUrl, product.getImage());
assertEquals(linkUrl, product.getLink());
assertEquals(lPrice, product.getLprice());
assertEquals(0, product.getMyprice());
createdProduct = product; // 멤버 변수에 넘겨서 Order(2)에서 사용!
}
@Test
@Order(2)
@DisplayName("신규 등록된 관심상품의 희망 최저가 변경")
void test2() {
// given
Long productId = this.createdProduct.getId();
int myPrice = 70000;
ProductMypriceRequestDto requestDto = new ProductMypriceRequestDto(myPrice);
// when
Product product = productService.updateProduct(productId, requestDto);
// then
assertNotNull(product.getId());
assertEquals(userId, product.getUserId());
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
List<Product> productList = productService.getProducts(userId);
// then
// 1. 전체 상품에서 테스트에 의해 생성된 상품 찾아오기 (상품의 id 로 찾음)
Long createdProductId = this.createdProduct.getId();
Product foundProduct = productList.stream() // 전체 돌면서
.filter(product -> product.getId().equals(createdProductId)) // createdProductId랑 같은 거 찾아온다. (그것이 foundProduct에 들어옴)
.findFirst()
.orElse(null); // 이게 null이면 제대로 저장되지 않은 것 -> 에러!(테스트 코드 깨지는 것)
// 2. Order(1) 테스트에 의해 생성된 상품과 일치하는지 검증
assertNotNull(foundProduct);
assertEquals(userId, foundProduct.getUserId());
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());
}
}
testImplementation 'org.springframework.security:spring-security-test'
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
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();
}
}
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sparta.springcore.controller.ProductController;
import com.sparta.springcore.controller.UserController;
import com.sparta.springcore.dto.ProductRequestDto;
import com.sparta.springcore.model.User;
import com.sparta.springcore.model.UserRoleEnum;
import com.sparta.springcore.security.UserDetailsImpl;
import com.sparta.springcore.security.WebSecurityConfig;
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.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.context.WebApplicationContext;
import java.security.Principal;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;
// Controller를 테스트하는 것, 이를 위해 HTTP 통신이 필요함
// (client 요청 결과 확인하는 것)
// View, Controller 테스트한다.
@WebMvcTest(
// UserController, ProductController 따로 나눠도 됨
controllers = {UserController.class, ProductController.class},
excludeFilters = {
@ComponentScan.Filter(
type = FilterType.ASSIGNABLE_TYPE,
classes = WebSecurityConfig.class
)
}
)
class UserProductMvcTest {
// 스프링에서 제공해주는 MockMvc 사용해서 테스트를 진행할 것.
private MockMvc mvc;
private Principal mockPrincipal;
@Autowired
private WebApplicationContext context;
@Autowired
private ObjectMapper objectMapper;
// 가짜 빈 DI (dependency 끊어내는 것)
@MockBean
UserService userService;
@MockBean
KakaoUserService kakaoUserService;
@MockBean
ProductService productService;
@BeforeEach
public void setup() {
// MockSpringSecurityFilter 사용해서 세팅
// SpringSecurity에 가짜 사용자 정보를 넣어주기 위해 Filter 사용해서 Authentication
mvc = MockMvcBuilders.webAppContextSetup(context)
.apply(springSecurity(new MockSpringSecurityFilter()))
.build();
}
private void mockUserSetup() {
// Mock 테스트 샘플 유저 생성
String username = "제이홉";
String password = "hope!@#";
String email = "hope@sparta.com";
UserRoleEnum role = UserRoleEnum.USER;
User testUser = new User(username, password, email, role);
UserDetailsImpl testUserDetails = new UserDetailsImpl(testUser);
// UsernamePasswordAuthenticationToken 안에 UserDetailsImpl이 있었고 그 안에 User가 있음
// 최종적으로 만들어진 것을 mockPrincipal 보낼 때 담아서 보내줌(아래 test 3)
// 테스트동안만 스프링에 로그인 되어있다고 이야기해주기 위해 사용
// 이를 위해 MockSpringSecurityFilter 해주고, 모듈도 추가한 것
mockPrincipal = new UsernamePasswordAuthenticationToken(testUserDetails, "", testUserDetails.getAuthorities());
}
@Test
@DisplayName("로그인 view")
void test1() throws Exception {
// when - then
mvc.perform(get("/user/login")) // 스프링 mvc 실행하는 데 이 주소로 실행하고 이 메소드로 실행한다.(HTTP 통신을 실제로 보내는 것, Controller로 들어감)
.andExpect(status().isOk()) // HTTP의 status, isOk()면 200을 의미(HttpStatus.OK(200) -> HTTP 코드)
// 따라서 status code는 200이 나와야함
.andExpect(view().name("login")) // 스프링 mvc에서 View를 넘겨줌 (return 에서 나온 View와 정확해야함)
.andDo(print()); // HTTP의 header와 body를 프린트 해줌
}
@Test
@DisplayName("회원 가입 요청 처리")
void test2() throws Exception {
// given
// 회원가입 요청을 보내기 위해서는 @RequestParam -> 이것을 만들어주기 위해 폼 만들어줘야함(요청을 보내는 클라이언트 입장에서는 폼데이터)
// 폼을 만들기 위해 signupRequestForm
MultiValueMap<String, String> signupRequestForm = new LinkedMultiValueMap<>();
signupRequestForm.add("username", "제이홉");
signupRequestForm.add("password", "hope!@#");
signupRequestForm.add("email", "hope@sparta.com");
signupRequestForm.add("admin", "false");
// when - then
mvc.perform(post("/user/signup")
.params(signupRequestForm) // 위에 만든 폼을 넘겨줌 (파라미터, 그것을 UserController에서 requestDto로 받아짐)
)
.andExpect(status().is3xxRedirection()) // redirect로 하는 경우 isOk HTTP 코드가 isOk가 아니라 redirect에 맞는 3으로 시작하는 코드로 타서 이 범위에 들어오면 redirection이라고 보는 것
.andExpect(view().name("redirect:/user/login")) // View name 똑같은 거 넣는다
.andDo(print());
}
@Test
@DisplayName("신규 관심상품 등록")
void test3() throws Exception {
// given
// 스프링이 떴는데 로그인 하지 않는 상태에서 신규 상품 등록 안됨 스프링 시큐리티 때문에 막혀서
// 따라서 mockUserSetup으로 가짜 사용자를 만듦.
this.mockUserSetup(); // POST /api/products
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 = 77000;
ProductRequestDto requestDto = new ProductRequestDto(
title,
imageUrl,
linkUrl,
lPrice
);
// POST 요청 시에 RequestBody json으로 받아야함 (폼형태가 아님)
String postInfo = objectMapper.writeValueAsString(requestDto);
// when - then
mvc.perform(post("/api/products")
.content(postInfo) // json 형태를 담은 string을 content에 넣음(HTTP Body 부분)
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON) // JSON 형태로 받을 수 있어를 서버에게 알려줌
.principal(mockPrincipal)
)
.andExpect(status().isOk()) // 가짜로 ProductController 돌았다 치고 응답 온 것 가지고 isOk로 200 나오면 정상
.andDo(print());
}
}
import com.sparta.springcore.dto.ProductMypriceRequestDto;
import com.sparta.springcore.dto.ProductRequestDto;
import com.sparta.springcore.dto.SignupRequestDto;
import com.sparta.springcore.model.Product;
import com.sparta.springcore.model.User;
import com.sparta.springcore.model.UserRoleEnum;
import com.sparta.springcore.service.ProductService;
import com.sparta.springcore.service.UserService;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class UserProductIntegrationTest {
@Autowired
UserService userService;
@Autowired
PasswordEncoder passwordEncoder;
@Autowired
ProductService productService;
Long userId = null;
Product 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 = 77000;
ProductRequestDto requestDto = new ProductRequestDto(
title,
imageUrl,
linkUrl,
lPrice
);
// when
Exception exception = assertThrows(IllegalArgumentException.class, () -> {
productService.createProduct(requestDto, userId);
});
// then
assertEquals("회원 Id 가 유효하지 않습니다.", exception.getMessage());
}
@Test
@Order(2)
@DisplayName("회원 가입")
void test2() {
// given
String username = "르탄이";
String password = "nobodynoboy";
String email = "retan1@spartacodingclub.kr";
boolean admin = false;
SignupRequestDto signupRequestDto = new SignupRequestDto();
signupRequestDto.setUsername(username);
signupRequestDto.setPassword(password);
signupRequestDto.setEmail(email);
signupRequestDto.setAdmin(admin);
// when
User user = userService.registerUser(signupRequestDto);
// then
assertNotNull(user.getId());
assertEquals(username, user.getUsername());
assertTrue(passwordEncoder.matches(password, user.getPassword()));
assertEquals(email, user.getEmail());
assertEquals(UserRoleEnum.USER, user.getRole());
userId = user.getId();
}
@Test
@Order(3)
@DisplayName("가입한 회원 Id 로 신규 관심상품 등록")
void test3() {
// 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 = 77000;
ProductRequestDto requestDto = new ProductRequestDto(
title,
imageUrl,
linkUrl,
lPrice
);
// when
Product product = productService.createProduct(requestDto, userId);
// then
assertNotNull(product.getId());
assertEquals(userId, product.getUserId());
assertEquals(title, product.getTitle());
assertEquals(imageUrl, product.getImage());
assertEquals(linkUrl, product.getLink());
assertEquals(lPrice, product.getLprice());
assertEquals(0, product.getMyprice());
createdProduct = product;
}
@Test
@Order(4)
@DisplayName("신규 등록된 관심상품의 희망 최저가 변경")
void test4() {
// given
Long productId = this.createdProduct.getId();
int myPrice = 70000;
ProductMypriceRequestDto requestDto = new ProductMypriceRequestDto(myPrice);
// when
Product product = productService.updateProduct(productId, requestDto);
// then
assertNotNull(product.getId());
assertEquals(userId, product.getUserId());
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(5)
@DisplayName("회원이 등록한 모든 관심상품 조회")
void test5() {
// given
// when
List<Product> productList = productService.getProducts(userId);
// then
// 1. 전체 상품에서 테스트에 의해 생성된 상품 찾아오기 (상품의 id 로 찾음)
Long createdProductId = this.createdProduct.getId();
Product foundProduct = productList.stream()
.filter(product -> product.getId().equals(createdProductId))
.findFirst()
.orElse(null);
// 2. Order(1) 테스트에 의해 생성된 상품과 일치하는지 검증
assertNotNull(foundProduct);
assertEquals(userId, foundProduct.getUserId());
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());
}
}
import com.sparta.springcore.dto.SignupRequestDto;
import com.sparta.springcore.model.User;
import com.sparta.springcore.model.UserRoleEnum;
import com.sparta.springcore.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.Optional;
@Service
public class UserService {
private final PasswordEncoder passwordEncoder;
private final UserRepository userRepository;
private static final String ADMIN_TOKEN = "AAABnv/xRVklrnYxKZ0aHgTBcXukeZygoC";
@Autowired
public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}
public User registerUser(SignupRequestDto requestDto) {
// 회원 ID 중복 확인
String username = requestDto.getUsername();
Optional<User> found = userRepository.findByUsername(username);
if (found.isPresent()) {
throw new IllegalArgumentException("중복된 사용자 ID 가 존재합니다.");
}
// 패스워드 암호화
String password = passwordEncoder.encode(requestDto.getPassword());
String email = requestDto.getEmail();
// 사용자 ROLE 확인
UserRoleEnum role = UserRoleEnum.USER;
if (requestDto.isAdmin()) {
if (!requestDto.getAdminToken().equals(ADMIN_TOKEN)) {
throw new IllegalArgumentException("관리자 암호가 틀려 등록이 불가능합니다.");
}
role = UserRoleEnum.ADMIN;
}
User user = new User(username, password, email, role);
userRepository.save(user);
return user;
}
}
실제 현업에서는 테스트 코드가 매우 중요하다고 한다. 내가 프로젝트할 때도 써먹을 때가 있었으면 한다. 그리고 강의는 다 들었는데 정리를 미루고 있다. 빨리 정리하자!!!! 아즈아😬
출처: 스파르타코딩클럽