단위 테스트 순서
1. 더미데이터 준비(테스트를 위한 데이터를 준비한다. 직접 데이터를 테스트코드에서 넣어도 되고 FakeStroe,FakeUser 등의 클래스나 함수를 만들어 둬도 된다. 또는 spring data-faker 라이브러리를 이용해도 된다.
2. Test할때는 데이터베이스 새로 만들어서 해야한다. 실제 데이터베이스와의 연결을 끊어야함(실수조심!!)
- config파일여러개 만들어서 관리해야함(spring boot 기능)
3. 테스트 패키지에 테스트 파일을 생성한다.
4. 테스트 코드 작성
- 레포지토리 테스트의 목적은 올바른 쿼리, 좋은 쿼리가 나가도록 테스트하는 것이다.
- 기본적으로 JPA메소드들은 잘 만들어져 있다.
- 의도대로 쿼리가 나가는지, 과도한 쿼리, 쓸데없이 무거운 쿼리가 나가지는 않는지 테스트하며 레포지토리 메소드를 수정하는 것이 목적이다.(findBy00)
- lazy,eager 전략을 적합하게 사용하고, fetch join, jpql을 이용하여 최적화하여야 한다.
- left join, outer 조인등 나가는 쿼리를 조사하여 적합한 쿼리가 나가도록 수정
- 시나리오 맞게 테스트 또는 CRUD 테스트
- 테스트 어노테이션을 붙인다. (JPA의 경우 @DataJpaTest)
- 테스트할 레포지토리를 DI한다. (@Autowired, 생성자에 붙이는 것을 추천)
- 꼭 해당 레포지토리가 아닌 요구사항, API스펙에 따라 필요한 레포지토리를 사용하기도 한다. (Product 레포지토리 테스트에서 Optionp 레포지토리 메소드를 이용하듯, 상품과 옵션은 1대다 관계, 클라이언트에 반환시 두개의 값(상품,옵션)을 반환하며 옵션레포지토리에서 조회하는 것이 논리적으로 적합하다. -> 상품아이디로 해당상품의 옵션을 조회하는 것이기에)
- 테스트에서 공통적으로 처음에 실행되는 코드(더미데이터)를 모아 메소드를 만들고 @BeforeEach 어노테이션을 붙인다. (세팅)
- 테스트할 코드를 작성하고 메소드위에 @Test어노테이션을 붙인다.
- 상황에 따라 em.clear, em.flush 등을 사용하여 준영속화 하여 쿼리를 관찰한다.
- 꼭 필요한 것은 아니지만 ObjectMapper를 이용하여 영속화,lazy 테스트를 할 수 있다.
- om을 DI하여 아래코드처럼 직렬화 할 수있다. 직렬화는 lazy로딩이 일어나기 전에 일어나기 때문에 lazy테스트시 사용할 수 있다. 영속화되지 않았거나 프록시라면 이 때 에러가 발생한다.
String responseBody = om.writeValueAsString(productPG);
- BDD 방식으로 코드를 작성한다. 참고:https://yunzae.tistory.com/257
예시1) 상품
상품과 옵션은 1대다 이다. 아래 상황에서는 시나리오에 적합하게 테스트하기 위해 옵션 레포지토리의 메소드를 사용
package com.example.kakao.product;
import com.example.kakao._core.util.DummyEntity;
import com.example.kakao.product.option.Option;
import com.example.kakao.product.option.OptionJPARepository;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.assertj.core.api.Assertions;
import org.hibernate.Hibernate;
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.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
import org.springframework.context.annotation.Import;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import javax.persistence.EntityManager;
import java.util.List;
import java.util.stream.Collectors;
@DisplayName("상품 관련 JPA 쿼리 테스트")
@Import(ObjectMapper.class)
@DataJpaTest
public class ProductJPARepositoryTest extends DummyEntity {
private EntityManager em;
private ProductJPARepository productJPARepository;
private OptionJPARepository optionJPARepository;
private ObjectMapper om;
public ProductJPARepositoryTest(
@Autowired EntityManager em,
@Autowired ProductJPARepository productJPARepository,
@Autowired OptionJPARepository optionJPARepository,
@Autowired ObjectMapper om) {
this.em = em;
this.productJPARepository = productJPARepository;
this.optionJPARepository = optionJPARepository;
this.om = om;
}
@BeforeEach
public void setUp(){
List<Product> productListPS = productJPARepository.saveAll(productDummyList());
optionJPARepository.saveAll(optionDummyList(productListPS));
em.clear();
}
@Test
@DisplayName("전체 상품 조회 테스트")
public void product_findAll_test() throws JsonProcessingException {
// given
int page = 0;
int size = 9;
// when
PageRequest pageRequest = PageRequest.of(page, size);
Page<Product> productPG = productJPARepository.findAll(pageRequest);
String responseBody = om.writeValueAsString(productPG);
System.out.println("테스트 : "+responseBody);
// then
Assertions.assertThat(productPG.getTotalPages()).isEqualTo(2);
Assertions.assertThat(productPG.getSize()).isEqualTo(9);
Assertions.assertThat(productPG.getNumber()).isEqualTo(0);
Assertions.assertThat(productPG.getTotalElements()).isEqualTo(15);
Assertions.assertThat(productPG.isFirst()).isEqualTo(true);
Assertions.assertThat(productPG.getContent().get(0).getId()).isEqualTo(1);
Assertions.assertThat(productPG.getContent().get(0).getProductName()).isEqualTo("기본에 슬라이딩 지퍼백 크리스마스/플라워에디션 에디션 외 주방용품 특가전");
Assertions.assertThat(productPG.getContent().get(0).getDescription()).isEqualTo("");
Assertions.assertThat(productPG.getContent().get(0).getImage()).isEqualTo("/images/1.jpg");
Assertions.assertThat(productPG.getContent().get(0).getPrice()).isEqualTo(1000);
}
// ManyToOne 전략을 Eager로 간다면 추천
@DisplayName("개별 상품 상세 조회-eager전략")
@Test
public void option_findByProductId_eager_test() throws JsonProcessingException {
// given
int id = 1;
// when
// 충분한 데이터 - product만 0번지에서 빼면 된다
// 조인은 하지만, fetch를 하지 않아서, product를 한번 더 select 했다.
List<Option> optionListPS = optionJPARepository.findByProductId(id); // Eager
System.out.println("json 직렬화 직전========================");
String responseBody = om.writeValueAsString(optionListPS);
System.out.println("테스트 : "+responseBody);
// then
}
@DisplayName("개별 상품 상세 조회-lazy시 에러")
@Test
public void option_findByProductId_lazy_error_test() throws JsonProcessingException {
// given
int id = 1;
// when
// option을 select했는데, product가 lazy여서 없는 상태이다.
List<Option> optionListPS = optionJPARepository.findByProductId(id); // Lazy
// product가 없는 상태에서 json 변환을 시도하면 (hibernate는 select를 요청하는데, json mapper는 json 변환을 시도하게 된다)
// 이때 json 변환을 시도하는 것이 타이밍적으로 더 빠르다 (I/O)가 없기 때문에!!
// 그래서 hibernateLazyInitializer 오류가 발생한다.
// 그림 설명 필요
System.out.println("json 직렬화 직전========================");
String responseBody = om.writeValueAsString(optionListPS);
System.out.println("테스트 : "+responseBody);
// then
}
// 추천
// 조인쿼리 직접 만들어서 사용하기
@DisplayName("개별 상품 상세 조회-커스텀 쿼리 전략")
@Test
public void option_mFindByProductId_lazy_test() throws JsonProcessingException {
// given
int id = 1;
// when
List<Option> optionListPS = optionJPARepository.mFindByProductId(id); // Lazy
System.out.println("json 직렬화 직전========================");
String responseBody = om.writeValueAsString(optionListPS);
System.out.println("테스트 : "+responseBody);
// then
}
// 추천
@DisplayName("개별 상품 상세 조회")
@Test
public void product_findById_and_option_findByProductId_lazy_test() throws JsonProcessingException {
// given
int id = 1;
// when
System.out.println("======================start================");
Product productPS = productJPARepository.findById(id).orElseThrow(
() -> new RuntimeException("상품을 찾을 수 없습니다")
);
// product 상품은 영속화 되어 있어서, 아래에서 조인해서 데이터를 가져오지 않아도 된다.
List<Option> optionListPS = optionJPARepository.findByProductId(id); // Lazy
String responseBody1 = om.writeValueAsString(productPS);
String responseBody2 = om.writeValueAsString(optionListPS);
System.out.println("테스트 : "+responseBody1);
System.out.println("테스트 : "+responseBody2);
// then
}
}
package com.example.kakao.product.option;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
import java.util.Optional;
public interface OptionJPARepository extends JpaRepository<Option, Integer> {
List<Option> findByProductId(@Param("productId") int productId);
Optional<Option> findById(int id);
// findById_select_product_lazy_error_fix_test
@Query("select o from Option o join fetch o.product where o.product.id = :productId")
List<Option> mFindByProductId(@Param("productId") int productId);
}
예시2) 장바구니
package com.example.kakao.Cart;
import com.example.kakao._core.util.DummyEntity;
import com.example.kakao.cart.Cart;
import com.example.kakao.cart.CartJPARepository;
import com.example.kakao.product.Product;
import com.example.kakao.product.ProductJPARepository;
import com.example.kakao.product.option.Option;
import com.example.kakao.product.option.OptionJPARepository;
import com.example.kakao.user.User;
import com.example.kakao.user.UserJPARepository;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.assertj.core.api.Assertions;
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.orm.jpa.DataJpaTest;
import org.springframework.context.annotation.Import;
import javax.persistence.EntityManager;
import java.util.List;
@DisplayName("장바구니 관련 JPA 테스트")
@Import(ObjectMapper.class)
@DataJpaTest
public class CartJPARepositoryTest extends DummyEntity {
private EntityManager em;
private CartJPARepository cartJPARepository;
private UserJPARepository userJPARepository;
private ProductJPARepository productJPARepository;
private OptionJPARepository optionJPARepository;
private ObjectMapper om;
public CartJPARepositoryTest(@Autowired EntityManager em,
@Autowired CartJPARepository cartJPARepository,
@Autowired UserJPARepository userJPARepository,
@Autowired ObjectMapper om,
@Autowired ProductJPARepository productJPARepository,
@Autowired OptionJPARepository optionJPARepository) {
this.em = em;
this.cartJPARepository = cartJPARepository;
this.optionJPARepository = optionJPARepository;
this.userJPARepository = userJPARepository;
this.productJPARepository = productJPARepository;
this.om = om;
}
@BeforeEach
public void setUp(){
User user = newUser("user");
userJPARepository.save(user);
List<Product> productList = productJPARepository.saveAll(productDummyList());
List<Option> optionList = optionJPARepository.saveAll(optionDummyList(productList));
cartJPARepository.saveAll(cartDummyList(user,optionList,2));
em.clear();
}
@Test
@DisplayName("장바구니 담기 테스트")
public void cartSave_test() {
// given
User testuser = newUser("testuser");
userJPARepository.save(testuser);
Integer userid = testuser.getId();
Integer optionid=1;
Option testoption =optionJPARepository.findById(optionid).orElseThrow(
() -> new RuntimeException("해당 옵션을 찾을 수 없습니다.")
);
Cart cart = newCart(testuser,testoption,3);
long previousCount = cartJPARepository.count();
// when
System.out.println("======================start===================");
cartJPARepository.save(cart);
Integer cartid= cart.getId();
System.out.println("======================end======================");
// then
Assertions.assertThat(cartJPARepository.count()).isEqualTo(previousCount+1);
Cart savedCart = cartJPARepository.findByUserId(userid).orElseThrow(
()-> new RuntimeException("해당 cart를 찾을 수 없습니다."));
Assertions.assertThat(savedCart.getId()).isEqualTo(cartid);
Assertions.assertThat(savedCart.getUser().getId()).isEqualTo(userid);
Assertions.assertThat(savedCart.getOption().getId()).isEqualTo(optionid);
}
@Test
@DisplayName("장바구니 조회 테스트-lazy시 에러")
public void cartOptionUser_findByUserId_lazy_error_test() {
// given
Integer userid=1;
// when
System.out.println("====================start===================");
List<Cart> cartList = cartJPARepository.findAllByUserId(userid);
System.out.println("========================end=====================");
// then
Assertions.assertThatThrownBy(()-> om.writeValueAsString(cartList)).isInstanceOf(JsonProcessingException.class);
}
@Test
@DisplayName("장바구니 조회 테스트 lazy-커스텀쿼리(Cart, User, Option, Product fetchJoin)")
public void cartOptionUser_findByUserId_lazy_test1() throws JsonProcessingException {
// given
Integer userid=1;
// when
System.out.println("====================start===================");
List<Cart> cartList = cartJPARepository.mFindAllByUserId(userid);
String responseBody = om.writeValueAsString(cartList);
System.out.println("테스트 : "+responseBody);
System.out.println("========================end=====================");
// then
Assertions.assertThat(cartList.get(0).getPrice()).isEqualTo(20000);
Assertions.assertThat(cartList.get(0).getQuantity()).isEqualTo(2);
Assertions.assertThat(cartList.get(0).getOption().getProduct().getProductName()).isEqualTo("기본에 슬라이딩 지퍼백 크리스마스/플라워에디션 에디션 외 주방용품 특가전");
Assertions.assertThat(cartList.get(0).getOption().getOptionName()).isEqualTo("01. 슬라이딩 지퍼백 크리스마스에디션 4종");
Assertions.assertThat(cartList.get(0).getUser().getUsername()).isEqualTo("user");
}
// @Test
// @DisplayName("장바구니 조회 테스트 lazy- @EntityGraph(Cart, User, Option fetchJoin)")
// //Entity graph 사용시 product를 사용할 수 없다?
// // 아래 코드를 실행하면 Product를 제외한 객체들은 한번에 가져온다, Product를 따로 쿼리를 날린다.
// //-> JPQL말고 어케하지, EntityGraph에서 설정을 할수 있나? Attribute에 Product 넣으면 안돌아가는데..
// public void cartOptionUser_findByUserId_lazy_test2() throws JsonProcessingException {
// // given
// Integer userid=1;
// // when
// System.out.println("====================start===================");
// List<Cart> cartList = cartJPARepository.findAllByUserId(userid);
// String responseBody = om.writeValueAsString(cartList);
// System.out.println("테스트 : "+responseBody);
// System.out.println("========================end=====================");
// // then
// Assertions.assertThat(cartList.get(0).getPrice()).isEqualTo(20000);
// Assertions.assertThat(cartList.get(0).getQuantity()).isEqualTo(2);
// Assertions.assertThat(cartList.get(0).getOption().getProduct().getProductName()).isEqualTo("기본에 슬라이딩 지퍼백 크리스마스/플라워에디션 에디션 외 주방용품 특가전");
// Assertions.assertThat(cartList.get(0).getOption().getOptionName()).isEqualTo("01. 슬라이딩 지퍼백 크리스마스에디션 4종");
// Assertions.assertThat(cartList.get(0).getUser().getUsername()).isEqualTo("user");
// }
@Test
@DisplayName("주문하기(장바구니 수정) 테스트- 업데이트후 조회(처음 조회시 fetch join)")
public void cartUpdate1_test(){
// given
Integer cartid = 1;
// when
System.out.println("====================start===================");
Cart cart = cartJPARepository.mfindById(cartid).orElseThrow(
() -> new RuntimeException("장바구리르 찾을 수 없거나 비어있습니다.")
);
cart.update(30,900000);
em.flush();
System.out.println("====================end===================");
// then
Cart updatedCart = cartJPARepository.findById(cartid).orElseThrow(
() -> new RuntimeException("장바구리를 찾을 수 없거나 비어있습니다.")
);
Assertions.assertThat(updatedCart.getQuantity()).isEqualTo(30);
Assertions.assertThat(updatedCart.getPrice()).isEqualTo(900000);
Assertions.assertThat(updatedCart.getOption().getId()).isEqualTo(1);
Assertions.assertThat(updatedCart.getOption().getProduct().getId()).isEqualTo(1);
}
}
package com.example.kakao.cart;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
import java.util.Optional;
public interface CartJPARepository extends JpaRepository<Cart, Integer> {
// @EntityGraph(attributePaths = {"user", "option"}, type = EntityGraph.EntityGraphType.LOAD)
List<Cart> findAllByUserId(int userid);
Optional<Cart> findByUserId(int userid);
@Query("select c from Cart c join fetch c.user u join fetch c.option o join fetch o.product where c.user.id = :userId")
List<Cart> mFindAllByUserId(@Param("userId") int userId);
@Query("select c from Cart c join fetch c.option o join fetch o.product p where c.id = :cartId")
Optional<Cart> mfindById(@Param("cartId") int cartId);
// 한번에 두 컬럼을 업데이트는 못하나..?
@Modifying
@Query("update Cart c set c.quantity = :quantity where c.id = :cartId")
void updateQuantityById(@Param("cartId") int cartId,@Param("quantity") int quantity);
//업데이트 쿼리를 직접 날릴 때는 영속화된 객체가 동기화가 되지 않는다. 디비에는 바뀌었지만 기존객체에는 변화가 없다. -> 새로 영속화 해줘야 함
}
각각 테스트시 에러가 나지 않더라도 한번에 테스트를 하면 에러가 날 수 있다.
그 이유중 하나가 id번호가 꼬여서 이다.
그래서 setup()시 아래 코드를 넣어 번호를 초기화 시켜준다.
각 테스트 코드가 끝나면서 데이터를 없어지지만 id 시퀀스는 그대로인경우가 있다.
em.createNativeQuery("ALTER TABLE user_tb ALTER COLUMN id RESTART WITH 1").executeUpdate();
em.createNativeQuery("ALTER TABLE product_tb ALTER COLUMN id RESTART WITH 1").executeUpdate();
em.createNativeQuery("ALTER TABLE item_tb ALTER COLUMN id RESTART WITH 1").executeUpdate();
em.createNativeQuery("ALTER TABLE option_tb ALTER COLUMN id RESTART WITH 1").executeUpdate();
em.createNativeQuery("ALTER TABLE order_tb ALTER COLUMN id RESTART WITH 1").executeUpdate();
em.createNativeQuery("ALTER TABLE cart_tb ALTER COLUMN id RESTART WITH 1").executeUpdate();
// 데이터를 날아 가지만 저장되는 id sequence는 초기화되지 않는다.
'I leaned > 스프링,스프링부트' 카테고리의 다른 글
AOP(관점지향프로그래밍) (0) | 2023.07.25 |
---|---|
컨트롤러 단위 테스트 (0) | 2023.07.17 |
Open In View (0) | 2023.07.17 |
DTO (0) | 2023.07.17 |
컨트롤러와 서비스의 책임 (0) | 2023.07.17 |