I leaned/스프링,스프링부트

리포지토리 단위 테스트

윤재에요 2023. 7. 17. 21:21

단위 테스트 순서

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 패턴(테스트 코드작성)

BDD패턴(given, when, then) 행위 주도 개발 (Behavior Driven Development) BDD는 TDD를 근간으로 파생된 개발 방법이다. TDD(Test Driven Development)에서 한발 더 나아가 테스트 케이스 자체가 요구사양이 되도록 하는

yunzae.tistory.com

 

예시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는 초기화되지 않는다.