SpringBoot 테스트

유요한·2023년 2월 14일
0

Spring Boot

목록 보기
11/25
post-thumbnail

TDD와 단위 테스트는 다른 이야기입니다. TDD는 테스트가 주도하는 개발을 이야기합니다. 테스트 코드를 먼저 작성하는 것부터 시작합니다.

반면 단위 테스트는 TDD의 첫 번째 단계인 기능 단위의 테스트 코드를 작성하는 것을 이야기 합니다. TDD와 달리 테스트 코드를 꼭 먼저 작성해야 하는 것도 아니고, 리팩토링도 포함되지 않습니다. 순수하게 테스트 코드만 작성하는 것을 이야기 합니다.

단위 테스트 이점

  • 단위 테스트는 개발단계 초기에 문제를 발견하게 도와줍니다.

  • 단위 테스트는 개발자가 나중에 코드를 리팩토링하거나 라이브러리 업그레이드 등에서 기존 기능이 올바르게 작동하는지 확인할 수 있습니다.

  • 단위 테스트는 기능에 대한 불확실성을 감소시킬 수 있습니다.

  • 단위 테스트는 시스템에 대한 실제 문서를 제공합니다. 즉, 단위 테스트 자체가 문서로 사용할 수 있습니다.

테스트 코드 작성하기

작성하는 이유

  • 개발 과정에서 문제를 미리 발견할 수 있다.
  • 리팩토링의 리스크가 줄어든다.
  • 애플리케이션을 가동해서 직접 테스트하는 것보다 테스트를 빠르게 진행
  • 하나의 명세 문서로서의 기능을 수행
  • 몇 가지 프레임워크에 맞춰 테스트 코드를 작성하면 좋은 코드를 생산할 수 있다.
  • 코드가 작성된 목적을 명확하게 표현할 수 있으며, 불필요한 내용이 추가되는 것을 방지


MockMvc

Mock라는 단어를 사전에서 찾아보면 테스트를 위해 만든 모형을 의미한다. 따라서 테스트를 위해 실제 객체와 비슷한 모의 객체를 만드는 것을 모킹(Mocking)이라고 하며, 모킹한 객체를 메모리에서 얻어내는 과정을 목업(Mock-up)이라고 한다. MockMvc는 스프링 mvc의 통합테스트를 위한 라이브러리입니다. 자세히 말하자면, MockMvc는 웹 어플리케이션을 애플리케이션 서버에 배포하지 않고 테스트용 MVC환경을 만들어 요청 및 전송, 응답기능을 제공해주는 유틸리티 클래스입니다.

객체를 테스트하기 위해서는 당연히 테스트 대상 객체가 메모리에 있어야 한다. 하지만 생성하는데 복잡한 절차가 필요하거나 많은 시간이 소요되는 객체는 자주 테스트하기 어렵다. 또는 웹 애플리케이션의 컨트롤러처럼 WAS나 다른 소프트웨어의 도움이 필요한 객체도 있을 수 있다. 따라서 테스트하려는 실제 객체와 비슷한 가짜 객체를 만들어서 테스트에 필요한 기능만 가지도록 모킹을 하면 테스트가 쉬워진다. 그리고 테스트하려는 객체가 복잡한 의존성을 가지고 있을 때, 모킹한 객체를 이용하면, 의존성을 단절시킬 수 있어서 쉽게 테스트할 수 있다.

웹 애플리케이션에서 컨트롤러를 테스트할 때 서블릿 컨테이너를 모킹하기 위해서는 @WebMvcTest를 사용하거나 @AuotoConfigureMockMvc를 사용해야 합니다.

  • MockMvc.perform
    이 메소드는 MockMvcRequestBuilders를 매개변수로 받아 ResultActions를 return하는 메소드입니다. MockMvcRequestBuilders를 반환하는 정적 메소드로는 post(), get(), put(), delete() 등이 존재합니다. 이 메소드들은 HttpRequest를 만들어내기 위한 Builder로써 header, body 등을 지정하는 메소드들이 존재하며 이들은 다시 MockMvcRequestBuilders를 반환하기 때문에 간편하게 테스트를 위한 웹 요청을 만들 수 있습니다. 요청을 전송하는 역할을 합니다. 결과로 ResultActions 객체를 받으며, ResultActions 객체는 리턴 값을 검증하고 확인할 수 있는 andExcpect() 메소드를 제공해줍니다.

    MockMvcRequestBuilders 메서드들은 GET, POST, PUT, DELETE 요청 방식과 매핑되는 get(), post(), put(), delete() 메서드를 제공한다. 해당 메서드들은 MockHttpServletRequestBuilder의 메서들을 통해 MockHttpServletRequestBuilder 객체를 다시 리턴하여 메시지 체인을 구성하여 다양한 요청을 설정 가능하다.

  • get()
    HTTP 메소드를 결정할 수 있습니다. ( get(), post(), put(), delete() ) 인자로는 경로를 보내줍니다. 해당 url로 요청을 한다.
  • ResultActions.andExpect()
    요청의 결과로 예상(원하는) 응답을 지정함으로 실질적으로 테스트를 진행합니다. 즉, 응답을 검증하는 역할을 합니다. 응답코드, 본문에 포함되는 데이터, 헤더, 쿠기, 세션 등 응답에 포함되는 응답에 포함되는 전반적인 데이터들을 테스트할 수 있습니다.

  • contentType(MediaType.APPLICATION_JSON)
    Json 타입으로 지정

  • content()
    json으로 내용 등록

    예시)
    String jjson = "{\"name\": \"부대찌개\"}";
    .content(jjson)

  • ResultActions.andDo()
    요청/응답 전체 메세지를 확인할 수 있습니다. 그리고 mockMvc 요청을 한뒤 행동을 지정하는 메소드 입니다. 결과를 출력한다던지(print()) 로그를 출력하는 등의 행동을 지정할 수 있습니다.

  • .andReturn()
    return 결과를 반환할 때 쓰인다. 당연히 void 메소드를 테스트할 때 사용할 수 없다.
  1. MockMvc를 생성한다.
  2. MockMvc에게 요청에 대한 정보를 입력한다.
  3. 요청에 대한 응답값을 Expect를 이용하여 테스트한다.
  4. Expect가 모두 통과하면 테스트 통과
  5. Expect가 1개라도 실패하면 테스트 실패

post/body 예제

get 예제

patch/ param 예제

delete 예제

JsonPath

MockMvc 요청 설정 메소드

  • param / params : 쿼리 스트링 설정

  • cookie : 쿠키 설정

  • requestAttr : 요청 스코프 객체 설정

  • sessionAttr : 세션 스코프 객체 설정

  • content : 요청 본문 설정

  • header / headers : 요청 헤더 설정

  • contentType : 본문 타입 설정

    코드 예시

@Test
public void testController() throws Exception{
	
    mockMvc.perforem(get("test"))
    	.param("query", "부대찌개")
        .cookie("쿠키 값")
        .header("헤더 값:)
        .contentType(MediaType.APPLICATION.JSON)
        .content("json으로");
}
package com.example.rest_book1.controller;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;

import static org.hamcrest.Matchers.is;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;

// 테스트를 진행할때 JUnit에 내장된 실행자외에 다른 실행자를 실행시킨다.
// 여기서는 SpringExtension라는 실행자를 사용
// 즉, 스프링 부트 테스트와 JUnit 사이에 연결자 역할을 한다.
// JUnit4에서는 @RunWith였지만 5에서는 @ExtendWith다.
@ExtendWith(SpringExtension.class)
// 여러 스프링 테스트 어노테이션 중, Web(Spring MVC)에 집중할 수 있는 어노테이션
// 선언할 경우 @Controller, @ControllerAdivce 등을 사용 가능
// 단, @Service, @Component, @Repository 등은 사용할 수 없습니다.
// 여기서는 컨트롤러만 사용하기 때문에 선언
@WebMvcTest(controllers = HelloController.class)
class HelloControllerTest {

    // 스프링이 관리하는 Bean을 주입 받습니다.
    @Autowired
    // 웹 API를 테스트할 때 사용
    // 스프링 MVC 테스트의 시작점
    // 이 클래스를 통해 HTTP GET, POST 등에 대한 API 테스트를 할 수 있습니다.
    private MockMvc mvc;

    @Test
    @DisplayName("hello 리턴 테스트")
    void hello() throws Exception {
        String hello = "hello";

        // MockMvc를 통해 /hello주소로 HTTP GET 요청을 합니다.
        // 체이닝이 지원되어 아래와 같이 여러 검증 기능을 이어서 선언 가능
        mvc.perform(MockMvcRequestBuilders.get("/hello"))
                // mvc.perform 결과를 검증
                // HTTP Header의 Status를 검증
                // 우리가 흔히 알고 있는 200, 400, 500 등의 상태를 검증
                // 여기서는 OK 즉, 200인지 아닌지를 검증
                .andExpect(status().isOk())
                // 응답 본문의 내용을 검증
                // Controller에서 "hello"를 리턴하기 때문에 이 값이 맞는지 검증
                .andExpect(content().string(hello));
    }

    @Test
    @DisplayName("helloDto 리턴 테스트")
    public void helloDtoTest() throws Exception{
        String name = "hello";
        int amount = 1000;

        mvc.perform(MockMvcRequestBuilders.get("/hello/dto")
                // API 테스트 할 때 사용될 요청 파라미터를 설정합니다.
                // 단 값은 String만 허용됩니다.
                // 그래서 숫자/날짜 등의 데이터도 등록할 때는 문자열로 변경해야만 가능
                .param("name", name)
                .param("amount", String.valueOf(amount)))
                .andExpect(status().isOk())
                // jsonPath
                // - JSON 응답값을 필드별로 검증할 수 있는 메소드
                // - $를 기준으로 필드명을 명시합니다.
                // - 여기서는 name과 amount를 검증하니 $.name, $.amount로 검증
                .andExpect(jsonPath("$.name", is(name)))
                .andExpect(jsonPath("$.amount", is(amount)));

    }
}

단위 테스트와 통합 테스트

테스트 방법은 여러 기준으로 분류할 수 있습니다. 그 중 테스트 대상 범위를 기준으로 구분하면 크게 단위 테스트(Unit Test)와 통합 테스트(Integration Test)로 구분됩니다.

단위 테스트 특징

단위 테스트는 테스트 대상의 범위를 기준으로 가장 작은 단위의 테스트 방식입니다. 일반적으로 메서드 단위로 테스트를 수행하게 되며, 메서드 호출을 통해 의도한 결괏값이 나오는지 확인하는 수준으로 테스트를 진행합니다. 단위 테스트는 테스트 비용이 적게 들기 때문에 테스트 피드백을 빠르게 받을 수 있습니다. 다른 객체들과 의존관계를 맺고 있는데 어떻게 그거만 테스트할 수 있을까? 그것은 Bean Container에 주입된 실제 객체들을 가져다 쓰는 방법이 있고 Mock 객체를 만들어서 쓰는 방법이 있습니다.

예시

package com.example.rest_book1.dto;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

class HelloResponseDTOTest {

    @Test
    @DisplayName("룸북 기능 테스트")
    public void lombokTest() {
        // given
        String name = "test";
        int amount = 1000;

        // when
        HelloResponseDTO dto = new HelloResponseDTO(name, amount);

        // then

        // assertj라는 테스트 검증 라이브러리의 검증 메소드
        // 검증하고 싶은 대상을 메소드 인자로 받습니다.
        // 메소드 체이닝이 지원되어 isEqualTo와 같은 메소드를 이어서 사용할 수 있습니다.
        // isEqualTo : 동등 비교 메소드
        // assertThat에 있는 값과 isEqualTo의 값을 비교해서 같을 때만 성공
        Assertions.assertThat(dto.getName()).isEqualTo(name);
        Assertions.assertThat(dto.getAmount()).isEqualTo(amount);
    }

}
  • @ExtendsWith : 지금처럼 Service 영역에 대한 단위테스트를 위해서 사용하면 된다.

  • @Mock : Mock이란 "실제 객체를 만들어 사용하기에 시간, 비용 등의 Cost가 높거나 혹은 객체 서로간의 의존성이 강해 구현하기 힘들 경우 가짜 객체를 만들어 사용하는 방법이다." 라고 정의 되어 있다. 정의 에서 말하듯이 @Mock은 테스트를 필요한 가짜 객체이다.

  • @Test : 테스트 함수를 지정

가짜 객체임을 명시하기 위해 테스트 클래스에서 Mockito 클래스를 사용함을 알려주기 위해 @ExtendWith(MockitoExtension.class)어노테이션을 붙여준다.
위에서 보면 테스트 클래스명 위에 @ExtendWith(MockitoExtension.class)를 붙였습니다.

@Mock 어노테이션을 통해 Mock 클래스로 생성해야하는 가짜 객체임을 지정한다.

예시2

package com.example.shopping_.repository;

import com.example.shopping_.constant.ItemSellStatus;
import com.example.shopping_.entity.Item;
import com.example.shopping_.entity.QItem;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
import org.apache.juli.logging.Log;
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 org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.test.context.TestPropertySource;
import org.thymeleaf.util.StringUtils;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.time.LocalDateTime;
import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
@TestPropertySource(locations = "classpath:application-test.properties")
class ItemRepositoryTest {
    @Autowired
    ItemRepository itemRepository;

    // 영속성 컨텍스트를 이용하기 위해 @PersistenceContext 어노테이션을 이용해
    // EntityManager 빈을 주입합니다.
    @PersistenceContext
    // Entity를 관리하는 역할을 수행하는 클래스
    // EntityManager 내부에 영속성 컨텍스트를 이용하여 관리한다.
    // Transaction 단위를 수행할 때마다 생성된다.
    // 요청 시 생성되며 Transaction 후에는 close()되어야 한다.
    EntityManager em;


    @Test
    @DisplayName("상품 저장 테스트")
    public void createItemTest() {
        Item item = new Item();
        item.setItemNm("테스트 상품");
        item.setPrice(10000);
        item.setItemDetail("테스트 상품 상세 설명");
        item.setItemSellStatus(ItemSellStatus.SELL);
        item.setStockNumber(100);
        item.setRegTime(LocalDateTime.now());
        item.setUpdateTime(LocalDateTime.now());
        Item savedItem = itemRepository.save(item);
        System.out.println(savedItem.toString());
    }

    @Test
    @DisplayName("상품리스트 테스트")
    public void createItemList() {
        for (int i = 1; i < 10; i++) {
            Item item = new Item();
            item.setItemNm("테스트 상품" + i);
            item.setPrice(10000 + i);
            item.setItemDetail("테스트 상품 상세 설명" + i);
            item.setItemSellStatus(ItemSellStatus.SELL);
            item.setStockNumber(100);
            item.setRegTime(LocalDateTime.now());
            item.setUpdateTime(LocalDateTime.now());
            Item savedItem = itemRepository.save(item);
        }
    }

    @Test
    @DisplayName("상품명 조회 테스트")
    public void findByItemNmTest() {
        this.createItemList();
        List<Item> itemList = itemRepository.findByItemNm("테스트 상품1");
        for (Item item: itemList
             ) {
            System.out.println(item.toString());
        }
    }

    @Test
    @DisplayName("상품명, 상품상세설명 or 테스트")
    public void findByItemNmOrItemDetailTest() {
        // 기존에 만들었던 테스트 상품을 만드는 메소드를 실행하여 조회할 대상 만듬
        this.createItemList();
        // 상품명이 "테스트 상품1" 또는 상품 상세 설명이 "테스트 상품 상세 설명5"이면
        // 해당 상품을 itemList에 할당합니다.
        // 테스트 코드를 실행하면 조건대로 2개의 상품이 출력됩니다.
        List<Item> itemList = itemRepository.findByItemNmOrItemDetail("테스트 상품1", "테스트 상품 상세 설명5");
        for (Item item: itemList
             ) {
            System.out.println(item.toString());
        }
    }

    @Test
    @DisplayName("가격 LessThan 테스트")
    public void findByPriceLessThanTest() {
        this.createItemList();
        // 현재 데이터베이스에 저장된 가격이 10001 ~ 10010입니다.
        // 테스트 코드 실행 시 10개의 상품을 저장하는 로그가 콘솔에 나타나고
        // 맨 마지막에 가격이 10005보다 작은 4개의 상품을 출력해줍니다.
        List<Item> itemList = itemRepository.findByPriceLessThan(10005);
        for (Item item: itemList
             ) {
            System.out.println(item.toString());
        }
    }

    @Test
    @DisplayName("가격 내림차순 조회 테스트")
    public void findByPriceLessThanOrderByPriceDescTest() {
        this.createItemList();
        List<Item> itemList = itemRepository.findByPriceLessThanOrderByPriceDesc(10005);
        for (Item item: itemList
             ) {
            System.out.println(item.toString());
        }
    }

    @Test
    @DisplayName("@Query를 이용한 상품 조회 테스트")
    public void findByItemDetailTest() {
        this.createItemList();
        List<Item> itemList = itemRepository.findByItemDetail("테스트 상품 상세 설명");
        for (Item item: itemList
             ) {
            System.out.println(item.toString());
        }
    }

    @Test
    @DisplayName("Querydsl 조회 테스트1")
    public void queryDslTest() {
        this.createItemList();
        // JPAQueryFactory를 이용하여 쿼리를 동적으로 생성합니다.
        // 생성자의 파라미터로는 EntityManager 객체를 넣어줍니다.
        JPAQueryFactory queryFactory = new JPAQueryFactory(em);
        // Querydsl을 통해 쿼리를 생성하기 위해 플러그인을 통해 자동으로 생성된 QItem 객체를 이용합니다.
        QItem qItem = QItem.item;
        // 자바 소스코드지만 SQL문과 비슷하게 소스를 작성할 수 있습니다.
        JPAQuery<Item> query = queryFactory.selectFrom(qItem)
                .where(qItem.itemSellStatus.eq(ItemSellStatus.SELL))
                .where(qItem.itemDetail.like("%" + "테스트 상품 상세 설명" + "%"))
                .orderBy(qItem.price.desc());

        // JPAQuery 메소드 중 하나인 fetch를 이용해서 쿼리 결과를 리스트로 반환합니다.
        // fetch() 메소드 실행 시점에 쿼리문이 실행됩니다.
        List<Item> itemList = query.fetch();

        for (Item item: itemList
             ) {
            System.out.println(item.toString());
        }
    }

    // 상품 데이터를 만드는 새로운 메소드를 하나 만듭니다.
    // 1번부터 5번 상품은 상품의 판매상태를 SELL(판매 중)으로 지정하고
    // 6번부터 10번까지는 판매상태를 SOLD_OUT(품절)로 세팅해 생성합니다.
    public void createItemList2() {
        for (int i = 1; i < 5; i++) {
            Item item = new Item();
            item.setItemNm("테스트 상품" + i);
            item.setPrice(10000 + i);
            item.setItemDetail("테스트 상품 상세 설명 " +i);
            item.setItemSellStatus(ItemSellStatus.SELL);
            item.setStockNumber(100);
            item.setRegTime(LocalDateTime.now());
            item.setUpdateTime(LocalDateTime.now());
            itemRepository.save(item);
        }

        for (int i = 6; i < 10; i++) {
            Item item = new Item();
            item.setItemNm("테스트 상품" + i);
            item.setPrice(10000 + i);
            item.setItemDetail("테스트 상품 상세 설명 " +i);
            item.setItemSellStatus(ItemSellStatus.SOLD_OUT);
            item.setStockNumber(0);
            item.setRegTime(LocalDateTime.now());
            item.setUpdateTime(LocalDateTime.now());
            itemRepository.save(item);
        }
    }


    @Test
    @DisplayName("상품 Querydsl 조회 테스트2")
    public void queryDslTest2() {
        this.createItemList2();

        // BooleanBuilder는 쿼리에 들어갈 조건을 만들어주는 빌더라고 생각하면 됩니다.
        // Predicate를 구현하고 있으며 메소드 체인 형식으로 사용할 수 있습니다.
        BooleanBuilder booleanBuilder = new BooleanBuilder();
        QItem item = QItem.item;
        String itemDetail = "테스트 상품 상세 설명";
        int price = 10003;
        String itemSellStat ="SELL";

        // 필요한 상품을 조회하는데 필요한 "and" 조건을 추가하고 있습니다.
        // 아래 소스에서 상품의 판매상태가 SELL일 때만 booleanBuilder에 판매상태 조건을 동적으로 추가흔 것을 볼수 있다.
        booleanBuilder.and(item.itemDetail.like("%" + itemDetail +"%"));
        booleanBuilder.and(item.price.gt(price));

        if(StringUtils.equals(itemSellStat, ItemSellStatus.SELL)) {
            booleanBuilder.and(item.itemSellStatus.eq(ItemSellStatus.SELL));
        }

        // 데이터를 페이징해 조회하도록 PageRequest.of() 메소드를 이용해 Pageble 객체를 생성
        // 첫 번째 인자는 조회활 페이지 정보, 두 번째 인자는 한 페이지당 조회할 데이터 개수를 넣어줍니다.
        Pageable pageable = PageRequest.of(0, 5);

        // QueryDslPredicateExecutor 인터페이스에서 정의한 findAll() 메소드를 이용해 조건에 맞는 데이터를
        // Page 객체로 받아옵니다.
        Page<Item> itemPagingResult = itemRepository.findAll(booleanBuilder, pageable);
        System.out.println("total elements : " + itemPagingResult.getTotalElements());

        List<Item> resultItemList = itemPagingResult.getContent();

        for (Item resultItem: resultItemList
             ) {
            System.out.println(resultItem.toString());
        }
    }
}

@TestPropertySource(locations = "classpath:application-test.properties") 이건 기존에 저는 MySQL DB를 사용했는데 테스트 용으로는 H2를 사용했습니다. 그래서 기존의 application.properties를 두고 application-test.properties를 만들었습니다. 이 설정은 application-test.properties에 적힌 H2를 연결한 것을 불러오는 설정입니다.

application-test.properties

# DataSource ??
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:mem:test
spring.datasource.username=sa
spring.datasource.password=

# H2 jpa를 사용하려면
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
package com.example.velog.mapper;

import com.example.velog.domain.UserDTO;
import org.apache.commons.logging.Log;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.transaction.annotation.Transactional;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
// H2 데이터베이스 사용하기 위해서 설정
class UserMapperTest {

    @Autowired
    private UserMapper userMapper;

    @Test
    @DisplayName("회원가입 테스트")
    @Transactional
    void signUp() {
        String email = "zxzz45@naver.com";
        String password = "Dbekdms147!";
        String name = "테스터";
        String addr = "서울시 xxxx xxx";
        String addrDetail = "xxxx";
        String addrEtc = "2층";

        UserDTO p1 = UserDTO.builder()
                .userEmail(email)
                .userPw(password)
                .userName(name)
                .userAddr(addr)
                .userAddrDetail(addrDetail)
                .userAddrEtc(addrEtc)
                .build();

        Assertions.assertThat(p1.getUserEmail()).isEqualTo(email);
        Assertions.assertThat(p1.getUserPw()).isEqualTo(password);
        Assertions.assertThat(p1.getUserName()).isEqualTo(name);
        Assertions.assertThat(p1.getUserAddr()).isEqualTo(addr);
    }
}

예시3

package com.example.rest_book1.domain;

import lombok.extern.log4j.Log4j2;
import org.aspectj.lang.annotation.After;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import java.time.LocalDateTime;
import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

@ExtendWith(SpringExtension.class)
@SpringBootTest
@Log4j2
class PostsRepositoryTest {


    @Autowired
    PostsRepository postsRepository;

    @AfterEach
    public void cleanUp() {
        postsRepository.deleteAll();
    }

    @Test
    @DisplayName("게시글 저장 불러오기")
    public void saveBoardTest() {
        // given
        String title = "테스트 게시글";
        String content = "테스트 본문";

        // 테이블에 posts에 insert/update 쿼리를 실행합니다.
        // id 값이 있다면 update, 없다면 insert 쿼리가 실행됩니다.
        postsRepository.save(Posts.builder()
                .title(title)
                .content(content)
                .author("tester@naver.com")
                .build());

        // when
        // 테이블 posts에 있는 모든 데이터를 조회해오는 메소드입니다.
        List<Posts> postsList = postsRepository.findAll();

        // then
        Posts posts = postsList.get(0);
        log.info("게시물 보기 : " +posts);
        Assertions.assertThat(posts.getTitle()).isEqualTo(title);
        Assertions.assertThat(posts.getContent()).isEqualTo(content);
    }

    @Test
    public void BaseTimeEntityTest() {
        // given
        LocalDateTime now = LocalDateTime.of(2022,3, 7,6,51);
        postsRepository.save(Posts.builder()
                        .title("title")
                        .content("content")
                        .author("author")
                .build());

        // when
        List<Posts> postsList = postsRepository.findAll();

        // then
        Posts posts = postsList.get(0);

        log.info(">>>>>>>>> createDate = "
                + posts.getCreatedDate()
                + ", modifiedDate = "
                + posts.getModifiedDate());

        // isAfter : 날짜 비교
        Assertions.assertThat(posts.getCreatedDate()).isAfter(now);
        Assertions.assertThat(posts.getModifiedDate()).isAfter(now);
    }
}

통합 테스트의 특징

통합 테스트는 모듈을 통합하는 과정에서의 호환성 등을 포함해 애플리케이션이 정상적으로 동작하는지 확인하기 위해 수행하는 테스트 방식입니다. 앞에서 언급한 단위 테스트와 비교하자면 단위 테스트는 모듈을 독립적으로 테스트하는 반면 통합 테스트는 여러 모듈을 함께 테스트해서 정상적인 로직 수행이 가능한지를 확인합니다. 그리고 단위 테스트는 일반적으로 특정 모듈에 대한 테스트만 진행하기 때문에 데이터베이스나 네트워크 같은 외부 요인들을 제외하고 진행하는 데 비해 통합 테스트는 외부 요인들을 포함하고 테스트를 진행하므로 애플리케이션이 온전히 동작하는지를 테스트하게 됩니다. 다만 수행할 때마다 모든 컴포넌트가 동작해야 하기 때문에 테스트 비용이 커지는 단점이 있습니다.

예시1

package com.example.rest_book1.controller;

import com.example.rest_book1.dto.HelloResponseDTO;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

// 컨트롤러를 JSON을 반환하는 컨트롤러로 만들어준다.
// 메소드마다 @ResponseBody를 붙여도 되지만
// 클래스에 @RestController를 쓰면 한번에 해결할 수 있다.
@RestController
public class HelloController {
    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }

    @GetMapping("/hello/dto")
    public HelloResponseDTO helloDto(@RequestParam("name") String name,
                                     @RequestParam("amount") int amount) {
        return new HelloResponseDTO(name, amount);
    }
}
package com.example.rest_book1.controller;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;

import static org.hamcrest.Matchers.is;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;

// 테스트를 진행할때 JUnit에 내장된 실행자외에 다른 실행자를 실행시킨다.
// 여기서는 SpringExtension라는 실행자를 사용
// 즉, 스프링 부트 테스트와 JUnit 사이에 연결자 역할을 한다.
// JUnit4에서는 @RunWith였지만 5에서는 @ExtendWith다.
@ExtendWith(SpringExtension.class)
// 여러 스프링 테스트 어노테이션 중, Web(Spring MVC)에 집중할 수 있는 어노테이션
// 선언할 경우 @Controller, @ControllerAdivce 등을 사용 가능
// 단, @Service, @Component, @Repository 등은 사용할 수 없습니다.
// 여기서는 컨트롤러만 사용하기 때문에 선언
@WebMvcTest(controllers = HelloController.class)
class HelloControllerTest {

    // 스프링이 관리하는 Bean을 주입 받습니다.
    @Autowired
    // 웹 API를 테스트할 때 사용
    // 스프링 MVC 테스트의 시작점
    // 이 클래스를 통해 HTTP GET, POST 등에 대한 API 테스트를 할 수 있습니다.
    private MockMvc mvc;

    @Test
    @DisplayName("hello 리턴 테스트")
    void hello() throws Exception {
        String hello = "hello";

        // MockMvc를 통해 /hello주소로 HTTP GET 요청을 합니다.
        // 체이닝이 지원되어 아래와 같이 여러 검증 기능을 이어서 선언 가능
        mvc.perform(MockMvcRequestBuilders.get("/hello"))
                // mvc.perform 결과를 검증
                // HTTP Header의 Status를 검증
                // 우리가 흔히 알고 있는 200, 400, 500 등의 상태를 검증
                // 여기서는 OK 즉, 200인지 아닌지를 검증
                .andExpect(status().isOk())
                // 응답 본문의 내용을 검증
                // Controller에서 "hello"를 리턴하기 때문에 이 값이 맞는지 검증
                .andExpect(content().string(hello));
    }

    @Test
    @DisplayName("helloDto 리턴 테스트")
    public void helloDtoTest() throws Exception{
        String name = "hello";
        int amount = 1000;

        mvc.perform(MockMvcRequestBuilders.get("/hello/dto")
                // API 테스트 할 때 사용될 요청 파라미터를 설정합니다.
                // 단 값은 String만 허용됩니다.
                // 그래서 숫자/날짜 등의 데이터도 등록할 때는 문자열로 변경해야만 가능
                .param("name", name)
                .param("amount", String.valueOf(amount)))
                .andExpect(status().isOk())
                // jsonPath
                // - JSON 응답값을 필드별로 검증할 수 있는 메소드
                // - $를 기준으로 필드명을 명시합니다.
                // - 여기서는 name과 amount를 검증하니 $.name, $.amount로 검증
                .andExpect(jsonPath("$.name", is(name)))
                .andExpect(jsonPath("$.amount", is(amount)));

    }
}

예시2

package com.example.rest_book1.controller;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;

import static org.junit.jupiter.api.Assertions.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

// 테스트를 진행할때 JUnit에 내장된 실행자외에 다른 실행자를 실행시킨다.
// 여기서는 SpringExtension라는 실행자를 사용
// 즉, 스프링 부트 테스트와 JUnit 사이에 연결자 역할을 한다.
// JUnit4에서는 @RunWith였지만 5에서는 @ExtendWith다.
@ExtendWith(SpringExtension.class)
// 여러 스프링 테스트 어노테이션 중, Web(Spring MVC)에 집중할 수 있는 어노테이션
// 선언할 경우 @Controller, @ControllerAdivce 등을 사용 가능
// 단, @Service, @Component, @Repository 등은 사용할 수 없습니다.
// 여기서는 컨트롤러만 사용하기 때문에 선언
@WebMvcTest(controllers = HelloController.class)
class HelloControllerTest {

    // 스프링이 관리하는 Bean을 주입 받습니다.
    @Autowired
    // 웹 API를 테스트할 때 사용
    // 스프링 MVC 테스트의 시작점
    // 이 클래스를 통해 HTTP GET, POST 등에 대한 API 테스트를 할 수 있습니다.
    private MockMvc mvc;

    @Test
    void hello() throws Exception {
        String hello = "hello";

        // MockMvc를 통해 /hello주소로 HTTP GET 요청을 합니다.
        // 체이닝이 지원되어 아래와 같이 여러 검증 기능을 이어서 선언 가능
        mvc.perform(MockMvcRequestBuilders.get("/hello"))
                // mvc.perform 결과를 검증
                // HTTP Header의 Status를 검증
                // 우리가 흔히 알고 있는 200, 400, 500 등의 상태를 검증
                // 여기서는 OK 즉, 200인지 아닌지를 검증
                .andExpect(status().isOk())
                // 응답 본문의 내용을 검증
                // Controller에서 "hello"를 리턴하기 때문에 이 값이 맞는지 검증
                .andExpect(content().string(hello));
    }
}

여기서 assertj를 사용했는데 JUnit과 비교하여 assertj의 장점은 다음과 같습니다.

  • CoreMatchers와 달리 추가적으로 라이브러리가 필요하지 않습니다.

    JUnit의 assertThat을 쓰게 되면 is()와 같이 CoreMatchers 라이브러리가 필요합니다.

  • 자동완성이 좀더 확실하게 지원됩니다.

    IDE에서는 CoreMatchers와 같은 Matcher 라이브러리의 자동완성 지원이 약합니다.

Junit assert

JUint4.4 부터 assertThat 메서드가 추가됐다. 이 메서드는 hamcrest 라이브러리의 사용을 통합하며 assertions을 작성하는데 있어 더 나은 방법을 제공한다. Junit에서 AssertThat을 활용해 간단히 값을 비교할 수 있다.

선언형태

assertThat(T actual, Macher <? super T> matcher)
Assertions.assertThat(member.getName()).isEqualTo(fineMember.getName());

Junit assert메소드 정리

  • assertArrayEquals(a, b) : 배열 A와 B가 일치함을 확인한다.
  • assertEquals(a, b) : 객체 A와 B가 같은 값을 가지는지 확인한다.
  • assertEquals(a, b, c) : 객체 A와 B가 값이 일치함을 확인한다.( a: 예상값, b:결과값, c: 오차범위)
  • assertSame(a, b) : 객체 A와 B가 같은 객체임을 확인한다.
  • assertTrue(a): 조건 A가 참인지 확인한다.
  • assertNotNull(a) : 객채 A가 null이 아님을 확인한다.

AssertJ

멋진 테스트 코드를 작성하도록 돕는 AssertJ 라이브러리

AssertJ의 장점

  • 메소드 체이닝을 지원하기 때문에 좀 더 깔끔하고 읽기 쉬운 테스트 코드를 작성할 수 있습니다.
  • 개발자가 테스트를 하면서 필요하다고 상상할 수 있는 거의 모든 메소드를 제공합니다.

라이브러리 의존성 설정

Java8 이상 기반 프로젝트는 3.x 버전을, Java7 이하 기반 프로젝트는 2.x 버전을 사용하셔야 합니다.

Gradle

testCompile 'org.assertj:assertj-core:3.6.2'

Maven

<dependency>
  <groupId>org.assertj</groupId>
  <artifactId>assertj-core</artifactId>
  <!-- use 2.6.0 for Java 7 projects -->
  <version>3.6.2</version>
  <scope>test</scope>
</dependency>

AssertJ 메소드 임포트

다음과 같이 정적 임포트를 하면 AssertJ의 다양한 API를 클래스 이름없이 바로 사용할 수 있습니다.

import static org.assertj.core.api.Assertions.*;

테스트 대상 지정하기

모든 테스트 코드는 assertThat() 메소드에서 출발합니다. 다음과 같은 포멧으로 AssertJ에서 제공하는 다양한 메소드를 연쇄 호출 하면서 코드를 작성할 수 있습니다.

문자열 테스트

간단한 문자열 테스트 코드를 통해 AssertJ가 얼마나 강력한지 살펴봅시다.

// 주어진 "Hello, world! Nice to meet you."라는 문자열은
assertThat("Hello, world! Nice to meet you.") 
				.isNotEmpty() // 비어있지 않고
				.contains("Nice") // "Nice"를 포함하고
				.contains("world") // "world"도 포함하고
				.doesNotContain("ZZZ") // "ZZZ"는 포함하지 않으며
				.startsWith("Hell") // "Hell"로 시작하고
				.endsWith("u.") // "u."로 끝나며
                // "Hello, world! Nice to meet you."과 일치합니다.
				.isEqualTo("Hello, world! Nice to meet you."); 

숫자 테스트

대소 비교 뿐만 아니라, 오프셋을 이용하여 좀 더 느슨한 비교까지 가능합니다.

assertThat(3.14d) // 주어진 3.14라는 숫자는
				.isPositive() // 양수이고
				.isGreaterThan(3) // 3보다 크며
				.isLessThan(4) // 4보다 작습니다
				.isEqualTo(3, offset(1d)) // 오프셋 1 기준으로 3과 같고
				.isEqualTo(3.1, offset(0.1d)) // 오프셋 0.1 기준으로 3.1과 같으며
				.isEqualTo(3.14); // 오프셋 없이는 3.14와 같습니다

예시

package com.example.velog.mapper;

import com.example.velog.domain.UserDTO;
import org.apache.commons.logging.Log;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.transaction.annotation.Transactional;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
// H2 데이터베이스 사용하기 위해서 설정
class UserMapperTest {

    @Autowired
    private UserMapper userMapper;

    @Test
    @DisplayName("회원가입 테스트")
    @Transactional
    void signUp() {
        String email = "zxzz45@naver.com";
        String password = "Dbekdms147!";
        String name = "테스터";
        String addr = "서울시 xxxx xxx";
        String addrDetail = "xxxx";
        String addrEtc = "2층";

        UserDTO p1 = UserDTO.builder()
                .userEmail(email)
                .userPw(password)
                .userName(name)
                .userAddr(addr)
                .userAddrDetail(addrDetail)
                .userAddrEtc(addrEtc)
                .build();

        Assertions.assertThat(p1.getUserEmail()).isEqualTo(email);
        Assertions.assertThat(p1.getUserPw()).isEqualTo(password);
        Assertions.assertThat(p1.getUserName()).isEqualTo(name);
        Assertions.assertThat(p1.getUserAddr()).isEqualTo(addr);
    }
}


Controller는 해당 클래스에 대해서만 단위 테스트를 진행하면 되기 때문에 Controller 이외의 다른 객체들은 Mock 객체로 만들어 테스트를 진행

@WebMvcTest는 MVC 부분 슬라이스 테스트로, 보통 컨트롤러 하나만 테스트하고 싶을 때 사용한다. 그리고 @Controller, @RestController,@ControllerAdivce 같은 어노테이션이 붙은 Controller 관련 bean 들을 대상으로 load해줍니다. 그렇기 때문에 Controller 이외의 Service에 대해서는 MockBean을 통해 가짜객체를 주입해야 합니다. @WebMvcTest()의 프로퍼티로 테스트를 원하는 컨트롤러 클래스를 넣어준다.


이를 통해 특정 컨트롤러만 테스트 가능하도록 하는데, 해당 컨트롤러가 의존하는 빈이 있다면 @MockBean이나 @SpyBean을 사용해주어야 한다. 문제는 이렇게 특정 컨트롤러만을 빈으로 띄우고 @MockBean과 @SpyBean으로 특정 빈을 바꾸는 것은 새로운 애플리케이션 컨택스트를 필요로 한다.

이 어노테이션을 쓰는 경우에는 가끔 NoSuchBeanDefinitionException 오류가 나는데, @WebMvcTest는 @Controller같은 웹과 관련된 빈만 주입되며 @Service와 같은 일반적인 @Component는 생성되지 않는 특성 때문에 해당 컨트롤러를 생성하는 데 필요한 다른 빈을 정의하지 못해 발생한다. 따라서 이런 경우에는 @MockBean을 사용해서 필요한 의존성을 채워주어야 한다.

그러면 @SpringBootTest@WebMvcTest차이는 무엇일까?

SpringBoot에서 JUnit5을 사용하여 테스트 코드를 작성할 때 @SpringBootTest 어노테이션을 자주 쓰게 되는데, 상황에 따라서는 @WebMvcTest를 쓰는게 좋을 때도 있다.

@SpringBootTest는 프로젝트의 전체 컨텍스트를 로드하여 빈을 주입하기 때문에 속도가 느리고, 통합 테스트를 할 때 많이 사용한다. 수많은 스프링 빈을 등록하여 테스트에 필요한 의존성을 추가하기 때문에, 필요한 빈만을 등록하여 테스트를 진행하고자 한다면 슬라이스 테스트 어노테이션인 @WebMvcTest를 사용하는 것이 더 효율적이다.

@SpringBootTest
스프링 컨테이너와 테스트를 함께 실행한다. @SpringBootTest를 사용하면 손쉽게 통합 테스트를 위한 환경을 준비해준다. @SpringBootTest는 모든 빈들을 스캔하고 애플리케이션 컨텍스트를 생성하여 테스트를 실행한다. 스프링부트에서는 @SpringBootTest 어노테이션을 통해 애플리케이션 테스트에 필요한 거의 모든 의존성들을 제공해준다. @SpringBootTest 어노테이션은 Spring Main Application(@SpringBootApplication)을 찾아가 하위의 모든 Bean을 Scan한다. 그 후 Test용 Application Context를 만들면서 빈을 등록해주고, mock bean을 찾아가 그 빈만 mock bean으로 교체해준다. @SpringBootTest의 어노테이션에는 다양한 값을 줄 수 있는데, 이를 살펴보면 다음과 같다.

  • value와 properties: 애플리케이션 실행에 필요한 프로퍼티를 key=value 형태로 추가할 수 있음
  • args: 애플리케이션의 arguments로 값을 전달할 수 있음
  • classes: 애플리케이션을 로딩할 때 사용되는 컴포넌트 클래스들을 정의할 수 있음
  • webEnvironment: 웹 테스트 환경을 설정할 수 있음

ex)

이건 디폴트 값이라서 그냥 @SpringBootTest해도 위와 같은 의미이다.

@WebMvcTest와 @AutoConfigureMockMvc

@Transactional

테스트 케이스에 이 어노테이션이 있으면, 테스트 시작 전에 트랜잭션을 시작하고, 테스트 완료 후에 항상 롤백한다. 이렇게 하면 DB에 데이터가 남지 않으므로 다음 테스트에 영향을 주지 않는다

@RunWith(SpringRunner.class)

Mockito의 Mock 객체를 사용하기 위한 Annotation이다
class 위에 달아준다
@ExtendWith(SpringExtension.class)

Mockito의 Mock 객체를 사용하기 위한 Annotation이다
class 위에 달아준다

JUnit4에서는 RunWith(MockitoJUnitRunner.class)를,
JUnit5에서는 ExtendWith를 쓰도록 되어있다. 스프링부트 2.0 이상은 기본적으로 JUnit5로 버전이 되기때문에 ExtenWith를 사용하면 된다.

@WebMvcTest(UserController.class)
@ExtendWith(SpringExtension.class)
class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private UserService userService;

    @Test
    @DisplayName("회원가입 테스트")
    void signUp()throws Exception{
        mockMvc.perform(MockMvcRequestBuilders.get("/signUp"))
                .andExpect(status().isOk())
                .andDo(print())
                .andReturn();
    }
  }

테스트 코드 작성하는 방법

테스트 코드를 작성하는 방법은 다양합니다. 사람들이 많이 사용하는 Given-When-Then패턴과 F.I.R.S.T 전략이 있습니다. Given-When-Then가 BDD에 속합니다.

Given-When-Then 패턴은 간단한 테스트로 여겨지는 단위 테스트에서는 잘 사용하지 않습니다. 그 이유 중 하나는 불필요하게 코드가 길어진다는 것입니다. 하지만 이 패턴을 통해 테스트 코드를 작성한다면 명세 문서의 역할을 수행한다는 측면에서 많은 도움이 됩니다.

저는 TDD 방법을 선호합니다.

테스트 주도 개발(TDD)

테스트 주도 개발의 개발 주가

BDD

Behavior Driven Development의 약자로 TDD에서 따왔기 때문에 TDD추구하는 가치가 크게 다르지 않다. BDD를 처음으로 생각한 Danial Terhorst-North가 TDD를 수행하고 있던 도중 아래와 같은 생각을 했다고 합니다.

TDD하다가 해당 코드를 분석하기 위해서 많은 코드들을 분석해야하고 복잡성으로 인해 '누군가가 나에게 이 코드는 어떤식으로 짜여졌어!' 라고 말을 해줬으면 좋았을 텐데 라고 생각을 하다가 보니 행동 중심 개발을 하면 좋겠다고 생각했다.

BDD는 애플리케이션이 어떻게 행동해야 하는지에 대한 공통된 이해를 구성하는 방법입니다.

BDD의 행동

1. Narrative

모든 테스트 문장을 Narrative하게 되어야 한다. 즉, 코드보다 인간의 언어와 유사하게 구성되어야 한다. BDD는 TDD를 수행하려는 어떤한 행동과 기능을 개발자가 더 이해하기 쉽게하는 것이 목적이다. 모든 테스트 문장은 Given When Then으로 나눠서 작성할 수 있어야 한다.

2. Given/When/Then

Given

  • 테스트를 위해 주어진 상태
  • 테스트 대상에게 주어진 조건
  • 테스트가 동작하기 위해 주어진 환경

When

  • 테스트 대상에게 가해진 어떠한 상태
  • 테스트 대상에게 주어진 어떠한 조건
  • 테스트 대상의 상태를 변경시키기 위한 환경

Then

  • 앞선 과정의 결과

즉, 어떤 상태에서 출발(given)하여 어떤 상태이 변화를 가했을 때(when) 기대하는 어떠한 상태가 되어야 한다.(then)

예시

public class Calculator{
  public int plus(int a, int b){
    return a+b;
  }
}
public class CalculatorTest{
  Calculator calc = new Calculator();

  @Test
  void plus(){
    //given
    int a = 10;
    int b = 20;

    //when
    int result = calc.plus(a,b);

    //then
    assertEquals(result, a+b);
  }
}

여기서 보면 "어? 뭔 차이야?"라고 의문이 생길 수 있다.
근데 별 차이가 없다. 당연하다. BDD 자체가 TDD에서 더 새로운 개념이 아니라 TDD를 더 잘, 더 멋지게, 더 협조적으로 사용하기 위한 방법이기 때문이다.


테스트 케이스 작성

여태까지는 테스트에 대해서만 알아봤습니다. 저희가 테스트를 할 때 JUnit을 사용하는데 저는 JUnit5를 사용합니다.

개발한 기능을 실행해서 테스트 할 때 자바의 main 메서드를 통해서 실행하거나, 웹 애플리케이션의 컨트롤러를 통해서 해당 기능을 실행한다. 이러한 방법은 준비하고 실행하는데 오래 걸리고, 반복 실행하기 어렵고 여러 테스트를 한번에 실행하기 어렵다는 단점이 있다. 자바는 JUnit이라는 프레임워크로 테스를 실행해서 이러한 문제를 해결한다.

package com.example.hellospring.repository;

import com.example.hellospring.domain.Member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

import java.util.List;

public class MemoryMemberRepositoryTest {
    MemoryMemberRepository repositrory = new MemoryMemberRepository();

    /*
    *   한번에 여러 테스트를 실행하면 메모리 DB에 직전 테스트의 결과가 남을 수있다.
    *   이렇게 되면 다음 이전 테스트 때문에 다음 테스트가 실패할 수 있다.
    *   @afterEach를 사용하면 각 테스트가 종료될 때 마다 이 기능을 사용한다.
    *   여기에 데이터를 삭제하는 코드를 넣으면 삭제해주므로 테스트를 각각 독립적으로 실행한다.
    * */
    @AfterEach
    public void afterEach() {
        repositrory.clearStore();
    }

    @Test
    public void save() {
        Member member = new Member();
        member.setName("spring");

        repositrory.save(member);
        // Optional에서 값을 꺼낼 때는 get
        Member result = repositrory.findById(member.getId()).get();
        Assertions.assertThat(member).isEqualTo(result);
    }

    @Test
    public void findByName() {
        Member member1 = new Member();
        member1.setName("spring1");
        repositrory.save(member1);

        Member member2 = new Member();
        member2.setName("spring2");
        repositrory.save(member2);

        Member result = repositrory.findByName("spring1").get();
        Assertions.assertThat(result).isEqualTo(member1);
    }

    @Test
    public void findAll() {
        Member member1 = new Member();
        member1.setName("spring1");
        repositrory.save(member1);

        Member member2 = new Member();
        member2.setName("spring2");
        repositrory.save(member2);

        List<Member> result = repositrory.findAll();
        Assertions.assertThat(result.size()).isEqualTo(2);
    }
}

테스트 환경(JUnit)

  • 자바 프로그래밍 언어용 유닛 테스트 프레임워크
  • 가장 많이 사용되는 테스트 환경
  • 테스트 성공시 JUnit GUI 창에 녹색으로 표시 / 실패시 적색으로 표시
  • 하나하나의 케이스별로(단위로 나누어서) 테스트를 하는 단위 테스트 도구
  • JUnit이 되면 text할 수 있는 폴더가 생긴다.
  • 어노테이션 기반의 테스트 방식을 지원
  • 단정문(assert)을 통해 테스트 케이스 기댓값이 정상적으로 도출됐는지 검토할 수 있다.

JUnit(제이 유닛)은 자바 프로그래밍 언어용 단위 테스트 도구로 보이지 않고 숨겨진 단위 테스트를 끌어내어 정형화시켜 단위 테스트를 쉽게 해주는 테스트용 Framework입니다.  플러그인 형태로 Eclipse에 포함되어있으며, 하나의 jar 파일이 전부이기 때문에 사용법도 간단합니다. JUnit은 외부 테스트 프로그램(케이스)을 작성하여 번거롭게 디버깅하지 않아도 되며,  프로그램 테스트 시 걸릴 시간도 관리할 수 있는 기능을 가지고 있습니다. 테스트 결과를 확인하는 것 이외 최적화된 코드를 유추해내는 기능도 제공합니다.  또한, 테스트 결과를 단순한 텍스트로 남기는 것이 아니라 Test클래스로 남깁니다. 그래서 개발자에게 테스트 방법 및 클래스의 History를 넘겨줄 수도 있습니다.

JUint의 특징

  • @Test 메서드가 호출할 때마다 새로운 인스턴스가 생성되어 독립적인 테스트 가능
  • 단위 테스트 Framework 중 하나
  • 문자 혹은 GUI 기반으로 실행됨
  • 단정 문으로 테스트 케이스의 수행 결과를 판별함(assertEquals(예상 값, 실제 값))
  • JUnit4부터는 어노테이션으로 간결하게 테스트를 지원함
  • 결과는 성공(녹색), 실패(붉은색) 중 하나로 표시
  • 테스트 결과를 확인하는 것 이외 최적화된 코드를 유추해내는 기능도 제공

JUint5 Assertions

Assertion이 한글 뜻으로 주장이라는 뜻인데 테스트가 원하는 결과를 제대로 리턴하는지 에러는 발생하지 않는지 확인할 때 사용하는 메소드를 말합니다.

Assertions : 개발자가 테스트하고 싶은 인자값을 넣었을 때 예상한 결과가 나오는지 테스트 해볼 경우 사용

예 : "A"를 넣으면 "B"가 나온다

import org.junit.jupiter.api.Test;

import static java.time.Duration.ofSeconds;
import static org.junit.jupiter.api.Assertions.*;

public class AssertionsDemo {

    @Test
    void testAssertTrue() {
        assertTrue(1 == 1);
    }

    @Test
    void testAssertFalse() {
        assertFalse(1 != 1);
    }

    @Test
    void testAssertNull() {
        assertNull(null);
    }

    @Test
    void testAssertNotNull() {
        assertNotNull(1);
    }

    @Test
    void testAssertEquals() {
        assertEquals(1, 0 + 1);
    }

    @Test
    void testAssertThrows() {
        assertThrows(ArithmeticException.class, () -> {int i = 1/0;});
    }

    @Test
    void testAssertTimeout() {
        String actualResult = assertTimeout(ofSeconds(1), () -> {
            Thread.sleep(2000);
            return "a result";
        });

        assertEquals("a result", actualResult);
    }

    @Test
    void testAssertTimeoutPreemptively() {
        String actualResult = assertTimeoutPreemptively(ofSeconds(1), () -> {
            Thread.sleep(2000);
            return "a result";
        });

        assertEquals("a result", actualResult);
    }
}

JUnit 5 Assumptions

assumption은 한글로 추정이라는 뜻으로 메소드별 조건을 만족할 경우 진행시키고 아닌 경우 스킵하는 메소드입니다. 테스트 if 라고 생각하시면 될 거 같습니다.

Assumtptions : 개발자가 인자값을 정확히 모를 때 if 와 같은 용도로 사용

예 : 현재 테스트 환경이 "DEV"라면 테스트를 진행해라.

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assumptions.*;

public class AssumptionsDemo {

    @Test
    public void testAssumeTrue() {
        assumeTrue(true);
        // remainder of test
    }

    @Test
    public void testAssumeTrue2() {
        assumeTrue(false);
        // remainder of test
    }

    @Test
    public void testAssumeFalse() {
        assumeFalse(false);
        // remainder of test
    }

    @Test
    public void testAssumingThatTrue() {
        assumingThat(true, () -> {
            //실행 O
            System.out.println("success");
        });
    }

    @Test
    public void testAssumingThatFalse() {
        assumingThat(false, () -> {
            //실행 X
            System.out.println("success");
        });
    }
}

JUnit의 세부 모듈

JUnit의 생명주기

JUnit의 동작 방식을 확인하기 위해 생명주기를 알아보겠습니다. 생명주기와 관련되어 테스트 순서에 관여하게 되는 대표적인 어노테이션은 다음과 같습니다.

  • @Test : 테스트 코드를 포함한 메서드를 정의
  • @BeforeAll : 테스트를 시작하기 전에 호출되는 메서드 정의
  • @BeforeEach : 각 테스트 메서드가 실행되기 전에 동작하는 메서드를 정의
  • @AfterAll : 테스트를 종료하면서 호출되는 메서드 정의
  • @AfterEach : 각 테스트 메서드가 종료되면서 호출되는 메서드를 정의

@BeforeAll@AfterAll 어노테이션이 지정된 메서드는 전체 테스트 동작에서 처음과 마지막에만 각각 수행됩니다. @BeforeAll@AfterAll 어노테이션이 지정된 메서드는 각 테스트가 실행될 때 @Test 어노테이션이 지정된 테스트 메서드를 기준으로 실행되는 것을 볼 수 있습니다. @Disabled 어노테이션을 지정하면 이 어노테이션이 지정된 테스트는 실행되지 않습니다.

SpringBoot에서 JUnit5와 log

JUnit의 버전5를 뜻하며 2017년 2월에 출시되어 많은 개발자들이 사용하고 있는 테스팅 프레임워크입니다. JUnit 5는 이전 버전들과 다르게 3개의 서브 프로젝트 모듈로 이루어져있습니다.

JUnit5에서 JUnit Vintage 모듈을 포함하고 있어 JUnit 3, 4 문법을 사용할 수 있습니다. 하지만 완벽하게 지원해주는 것이 아니기 때문에 만약 사용한다하면 추가로 작업이 필요합니다.

JUnit5란?

SpringBoot 2.2.0 이전에는 JUnit4가 기본으로 설정되었지만, SpringBoot 2.2.0 버전부터는 JUnit5가 기본으로 설정됩니다. JUnit5는 런타임 시 Java8 이상이 필요하며, Junit5를 사용하려면 Gradle 4.7 이상이 여야 합니다. JUnit의 경우 Spring boot initializer에서 Spring-Web을 dependencies를 사용하게 되면 자동적으로 추가가 됩니다.

JUnit5 설정

Maven

// Maven
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    <version>5.5.2</version>
    <scope>test</scope>
</dependency>
  all {
        //logback과의 충돌 방지
        exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'
    }
    // JUnit4 제외시키기
    testImplementation ('org.springframework.boot:spring-boot-starter-test') {
        exclude group : 'org.junit.vintage', module: 'junit-vintage-engine'
    }
    // JUnit5 가져오기
    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.0'
    testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.9.0'
    testImplementation 'org.junit.jupiter:junit-jupiter-params:5.9.0'
    // log4j
    implementation 'org.springframework.boot:spring-boot-starter-log4j2'    
    

여기서 설정을 말했지만 Spring Boot 일정 버전 이상부터는 Junit5가 기본 설정입니다.

JUnit4와 5의 차이


컨트롤러 객체의 테스트

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;

// MockMvcRequestBuilders의 정적 메소드를 이용하여 RequestBuilder 객체를 만들어 perform에 인자로서 대입
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@ExtendWith(SpringExtension.class)
// Extension 사용을 위한 어노테이션 (ex BeforeAllCallback 등 Lifecycle callbacks 호출 시)
// JUnit4에서는 RunWith, JUnit5에서는 ExtendWith
// SpringExtension.class & MockitoExtension.class 두 종류가 있음
// SpringBootTest 없이 가볍게 테스트하고 싶을 때는 MockitoExtension, 일반적인 경우는 SpringExtension (좀 더 공부 필요)

@AutoConfigureMockMvc
// MockMvc 제어하는 어노테이션
// Mock : 테스트를 위해 실제 객체와 비슷한 객체를 만드는 것
// 같은 기능을 수행하는 어노테이션으로 @WebMvcTest 존재
// @WebMvcTest는 가볍게 테스트할 때 사용, @Controller 어노테이션만 테스트 가능
// @AutoConfigureMockMvc는 @Controller 뿐만 아니라 @Service, @Repository 모두 테스트 가능

@SpringBootTest
// 테스트에 필요한 거의 모든 의존성 제공
// 사용 시 @ExtendWith(SpringExtension.class) 이미 포함하고 있기 때문에 @ExtensionWith을 사용하지 않아도 됨
// Autowired 허용하여 객체 의존성 주입


class HelloControllerTest {

   @Autowired
   // MockMvc 객체 의존성 주입
   // SpringBootTest 어노테이션이 없다면 의존성 주입이 되지 않아 값이 null로 처리됨
   // 코드 입력 시 오류난 것 처럼 빨간 밑줄이 생기면서 " 'MockMvc' 타입의 bean을 찾을 수 없습니다. " 문구가 나오지만 정상 실행됨
   // 왜 실행되는지에 대해선 좀 더 공부 필요
   private MockMvc mvc;

   @Test
   // JUnit5의 테스트 수행
   // 여러 테스트 케이스 만들어서 값 안에 추가해서 테스트 가능
   public void helloControllerTest() throws Exception{
       mvc.perform(get("/hello")) // 만들어놓은 HelloController에 GET, POST 등의 메소드와 함께 Mock 이용하여 가상으로 접속
               .andExpect(status().isOk());   // 실행 결과 상태값 출력
       // 다른 여러 옵션 존재, 원하는 옵션을 추가해서 사용하면 됨
   }
}

서비스 객체의 테스트


레포지토리 객체의 테스트


JPA 레포지토리 테스트를 위한 @DataJpaTest 어노테이션

@DataJpaTest

JPA 레포지토리 테스트를 위해서는 @DataJpaTest를 이용할 수 있다. @DataJpaTest는 기본적으로 @Entity가 있는 엔티티 클래스들을 스캔하며 테스트를 위한 TestEntityManager를 사용해 JPA 레포지토리들을 설정해준다. 마찬가지로 @Component나 @ConfigurationProperties 빈들은 스캔되지 않는다.

여기서 주의해야 할 점은 슬라이스 테스트가 단위 테스트는 아니라는 점이다. 해당 어노테이션으로 테스트를 진행하면 테스트를 위한 애플리케이션 컨텍스트가 준비된다. 즉, 스프링이 준비되므로 해당 테스트들은 통합 테스트에 해당한다.

추가로 위에서 언급한 슬라이스 테스트 어노테이션 외에도 @JsonTest, @RestClientTest, @DataJdbcTest 등도 있다. 만약 json 관련 테스트를 위해 gson이나 objectMapper 등의 의존성이 필요하다면 @JsonTest를, RestTemplate이 필요하다면 @RestClientTest를 사용할 수 있다. 또한 Datasource와 JdbcTemplate만 필요하다면 @JdbcTest를 이용하면 된다.


JPA를 사용했을 때 테스트법

통합테스트

package com.example.rest_book1.service;

import com.example.rest_book1.domain.Posts;
import com.example.rest_book1.domain.PostsRepository;
import com.example.rest_book1.dto.PostsResponseDTO;
import com.example.rest_book1.dto.PostsSaveRequestsDTO;
import com.example.rest_book1.dto.PostsUpdateRequestDTO;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@RequiredArgsConstructor
@Service
public class PostsService {
    private final PostsRepository postsRepository;

    @Transactional
    public Long save(PostsSaveRequestsDTO postsSaveRequestsDTO) {
        return postsRepository.save(postsSaveRequestsDTO.toEntity()).getId();
    }

    @Transactional
    public Long update(Long id, PostsUpdateRequestDTO requestsDTO) {
        Posts posts = postsRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("해당 게시글이 없습니다. id = "+id));

        posts.update(requestsDTO.getTitle(), requestsDTO.getContent());

        return id;
    }

    public PostsResponseDTO findById(Long id) {
        Posts entity = postsRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("해당 게시글이 없습니다. id = "+id));

        return new PostsResponseDTO(entity);
    }
}
package com.example.rest_book1.controller;

import com.example.rest_book1.dto.PostsResponseDTO;
import com.example.rest_book1.dto.PostsSaveRequestsDTO;
import com.example.rest_book1.dto.PostsUpdateRequestDTO;
import com.example.rest_book1.service.PostsService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

@RequiredArgsConstructor
@RestController
public class PostsApiController {

    private final PostsService postsService;

    @PostMapping("/api/v1/posts")
    public Long save(@RequestBody PostsSaveRequestsDTO postsSaveRequestsDTO) {
        return postsService.save(postsSaveRequestsDTO);
    }

    @PutMapping("/api/v1/posts/{id}")
    public Long update(@PathVariable Long id, @RequestBody PostsUpdateRequestDTO requestsDTO) {
        return postsService.update(id,requestsDTO);
    }

    @GetMapping("/api/v1/posts/{id}")
    public PostsResponseDTO findById(@PathVariable Long id) {
        return postsService.findById(id);
    }
}
package com.example.rest_book1.controller;

import com.example.rest_book1.domain.Posts;
import com.example.rest_book1.domain.PostsRepository;
import com.example.rest_book1.dto.PostsSaveRequestsDTO;
import com.example.rest_book1.dto.PostsUpdateRequestDTO;
import lombok.extern.log4j.Log4j2;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Log4j2
class PostsApiControllerTest {

    @LocalServerPort
    private int port;

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private PostsRepository postsRepository;

    @AfterEach
    public void tearDown() throws Exception {
        postsRepository.deleteAll();
    }

    @Test
    @DisplayName("Posts_등록된다 테스트")
    public void test1() throws Exception {
        // given
        String title = "title";
        String content = "content";
        PostsSaveRequestsDTO requestsDTO = PostsSaveRequestsDTO.builder()
                .title(title)
                .content(content)
                .author("author")
                .build();

        String url = "http://localhost:" + port + "/api/v1/posts";

        // when
        // 기본 http 헤더를 사용하며 결과를 ResponseEntity로 반환받는다.
        ResponseEntity<Long> responseEntity = restTemplate.postForEntity(url, requestsDTO, Long.class);

        // then
        Assertions.assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
        Assertions.assertThat(responseEntity.getBody()).isGreaterThan(0L);

        List<Posts> all = postsRepository.findAll();
        log.info("-----------------------------------");
        log.info("확인 : " + all);
        log.info("-----------------------------------");
        Assertions.assertThat(all.get(0).getTitle()).isEqualTo(title);
        Assertions.assertThat(all.get(0).getContent()).isEqualTo(content);
    }

    @Test
    @DisplayName("Posts_수정된다 테스트")
    public void test2() {
        // given
        Posts savedPosts = postsRepository.save(Posts.builder()
                        .title("title")
                        .content("content")
                        .author("author")
                .build());

        Long updateId = savedPosts.getId();
        String expectedTitle = "title2";
        String expectedContent = "content2";

        PostsUpdateRequestDTO requestDTO = PostsUpdateRequestDTO.builder()
                .title(expectedTitle)
                .content(expectedContent)
                .build();

        String url = "http://localhost:" + port + "/api/v1/posts/" + updateId;

        // HttpEntity클래스는 HTTP요청또는 응답에 해당하는 HttpHeader와 HttpBody를 포함하는 클래스다.
        HttpEntity<PostsUpdateRequestDTO> requestEntity = new HttpEntity<>(requestDTO);

        // when
        ResponseEntity<Long> responseEntity = restTemplate.exchange(
                url, HttpMethod.PUT, requestEntity, Long.class);

        // then
        Assertions.assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
        Assertions.assertThat(responseEntity.getBody()).isGreaterThan(0L);

        List<Posts> all = postsRepository.findAll();
        log.info("-----------------------------------");
        log.info("수정확인 : " + all);
        Assertions.assertThat(all.get(0).getTitle()).isEqualTo(expectedTitle);
        Assertions.assertThat(all.get(0).getContent()).isEqualTo(expectedContent);

    }
}

단위 테스트

package com.example.rest_book1.domain;

import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

import javax.persistence.*;

// 실제 DB의 테이블과 매칭될 클래스이며 보통 Entity클래스라고 합니다.
// JPA를 사용하면 DB 데이터에 작업을 할경우 실제 쿼리를 날리기 보다는
// 이 Entity 클래스의 수정을 통해 작업을 합니다.

@Getter
@ToString
// 기본 생성자
@NoArgsConstructor
@Table(name = "posts")
// JPA 어노테이션
// 테이블과 링크될 클래스임을 나타냄
// 기본값으로 클래스의 카멜케이스 이름을 언더스코어 네이밍(_)으로 테이블 이름을 매칭
@Entity
public class Posts extends BaseTimeEntity {

    // 해당 테이블의 PK를 나타냄
    @Id
    // PK 생성 규칙을 나타낸다.
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    // 테이블의 칼럼을 나타내며 굳이 선언하지 않아도 해당 클래스의
    // 필드는 모두 칼럼이 됩니다.
    // 기본값외에도 추가로 변경이 필요한 옵션이 있으면 사용
    @Column(length = 500, nullable = false)
    private String title;

    @Column(columnDefinition = "TEXT", nullable = false)
    private String content;

    private  String author;

    // 생성자 상단에 선언 시 생성자에 포함된 필드만 빌더에 포함
    @Builder
    public Posts(String title, String content, String author) {
        this.title = title;
        this.content = content;
        this.author = author;
    }

    public void update(String title, String content) {
        this.title = title;
        this.content = content;
    }
}
package com.example.rest_book1.domain;

import com.example.rest_book1.domain.Posts;
import org.springframework.data.jpa.repository.JpaRepository;

public interface PostsRepository extends JpaRepository<Posts, Long> {
}
package com.example.rest_book1.domain;

import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;

@Getter
// JPA Entity 클래스들이 BaseTimeEntity을 상속할 경우
// 필드들(createdDate, modifiedDate)도 칼럼으로 인식하도록 합니다.
@MappedSuperclass
// BaseTimeEntity 클래스에 Auditing 기능을 포함시킵니다.
@EntityListeners(AuditingEntityListener.class)
public class BaseTimeEntity {

    /*
    *   @CreationTimestamp : 생성된 시간
        @Column(updatable = false) : 수정시에는 관여하지 않음
        @UpdateTimestamp : 업데이트가 발생했을 때 시간
    *   @Column(insertable = false) : 입력시에는 관여하지 않음
    * */
    // Entity가 생성되어 저장될 때 시간이 자동 저장
    @CreatedDate
    private LocalDateTime createdDate;

    // 조회한 Entity의 값을 변경할 때 시간이 자동 저장
    @LastModifiedDate
    private LocalDateTime modifiedDate;
}
package com.example.rest_book1.domain;

import lombok.extern.log4j.Log4j2;
import org.aspectj.lang.annotation.After;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import java.time.LocalDateTime;
import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

@ExtendWith(SpringExtension.class)
@SpringBootTest
@Log4j2
class PostsRepositoryTest {


    @Autowired
    PostsRepository postsRepository;

    @AfterEach
    public void cleanUp() {
        postsRepository.deleteAll();
    }

    @Test
    @DisplayName("게시글 저장")
    public void saveBoardTest() {
        // given
        String title = "테스트 게시글";
        String content = "테스트 본문";

        // 테이블에 posts에 insert/update 쿼리를 실행합니다.
        // id 값이 있다면 update, 없다면 insert 쿼리가 실행됩니다.
        postsRepository.save(Posts.builder()
                .title(title)
                .content(content)
                .author("tester@naver.com")
                .build());

        // when
        // 테이블 posts에 있는 모든 데이터를 조회해오는 메소드입니다.
        List<Posts> postsList = postsRepository.findAll();

        // then
        Posts posts = postsList.get(0);
        log.info("게시물 보기 : " +posts);
        Assertions.assertThat(posts.getTitle()).isEqualTo(title);
        Assertions.assertThat(posts.getContent()).isEqualTo(content);
    }

    @Test
    public void BaseTimeEntityTest() {
        // given
        LocalDateTime now = LocalDateTime.of(2022,3, 7,6,51);
        postsRepository.save(Posts.builder()
                        .title("title")
                        .content("content")
                        .author("author")
                .build());

        // when
        List<Posts> postsList = postsRepository.findAll();

        // then
        Posts posts = postsList.get(0);

        log.info(">>>>>>>>> createDate = "
                + posts.getCreatedDate()
                + ", modifiedDate = "
                + posts.getModifiedDate());

        // isAfter : 날짜 비교
        Assertions.assertThat(posts.getCreatedDate()).isAfter(now);
        Assertions.assertThat(posts.getModifiedDate()).isAfter(now);
    }
}

repository

package com.example.shopping_.repository;

import com.example.shopping_.constant.ItemSellStatus;
import com.example.shopping_.entity.Item;
import com.example.shopping_.entity.QItem;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
import org.apache.juli.logging.Log;
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 org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.test.context.TestPropertySource;
import org.thymeleaf.util.StringUtils;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.time.LocalDateTime;
import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
@TestPropertySource(locations = "classpath:application-test.properties")
class ItemRepositoryTest {
    @Autowired
    ItemRepository itemRepository;

    // 영속성 컨텍스트를 이용하기 위해 @PersistenceContext 어노테이션을 이용해
    // EntityManager 빈을 주입합니다.
    @PersistenceContext
    // Entity를 관리하는 역할을 수행하는 클래스
    // EntityManager 내부에 영속성 컨텍스트를 이용하여 관리한다.
    // Transaction 단위를 수행할 때마다 생성된다.
    // 요청 시 생성되며 Transaction 후에는 close()되어야 한다.
    EntityManager em;


    @Test
    @DisplayName("상품 저장 테스트")
    public void createItemTest() {
        Item item = new Item();
        item.setItemNm("테스트 상품");
        item.setPrice(10000);
        item.setItemDetail("테스트 상품 상세 설명");
        item.setItemSellStatus(ItemSellStatus.SELL);
        item.setStockNumber(100);
        item.setRegTime(LocalDateTime.now());
        item.setUpdateTime(LocalDateTime.now());
        Item savedItem = itemRepository.save(item);
        System.out.println(savedItem.toString());
    }

    @Test
    @DisplayName("상품리스트 테스트")
    public void createItemList() {
        for (int i = 1; i < 10; i++) {
            Item item = new Item();
            item.setItemNm("테스트 상품" + i);
            item.setPrice(10000 + i);
            item.setItemDetail("테스트 상품 상세 설명" + i);
            item.setItemSellStatus(ItemSellStatus.SELL);
            item.setStockNumber(100);
            item.setRegTime(LocalDateTime.now());
            item.setUpdateTime(LocalDateTime.now());
            Item savedItem = itemRepository.save(item);
        }
    }

    @Test
    @DisplayName("상품명 조회 테스트")
    public void findByItemNmTest() {
        this.createItemList();
        List<Item> itemList = itemRepository.findByItemNm("테스트 상품1");
        for (Item item: itemList
             ) {
            System.out.println(item.toString());
        }
    }

    @Test
    @DisplayName("상품명, 상품상세설명 or 테스트")
    public void findByItemNmOrItemDetailTest() {
        // 기존에 만들었던 테스트 상품을 만드는 메소드를 실행하여 조회할 대상 만듬
        this.createItemList();
        // 상품명이 "테스트 상품1" 또는 상품 상세 설명이 "테스트 상품 상세 설명5"이면
        // 해당 상품을 itemList에 할당합니다.
        // 테스트 코드를 실행하면 조건대로 2개의 상품이 출력됩니다.
        List<Item> itemList = itemRepository.findByItemNmOrItemDetail("테스트 상품1", "테스트 상품 상세 설명5");
        for (Item item: itemList
             ) {
            System.out.println(item.toString());
        }
    }

    @Test
    @DisplayName("가격 LessThan 테스트")
    public void findByPriceLessThanTest() {
        this.createItemList();
        // 현재 데이터베이스에 저장된 가격이 10001 ~ 10010입니다.
        // 테스트 코드 실행 시 10개의 상품을 저장하는 로그가 콘솔에 나타나고
        // 맨 마지막에 가격이 10005보다 작은 4개의 상품을 출력해줍니다.
        List<Item> itemList = itemRepository.findByPriceLessThan(10005);
        for (Item item: itemList
             ) {
            System.out.println(item.toString());
        }
    }

    @Test
    @DisplayName("가격 내림차순 조회 테스트")
    public void findByPriceLessThanOrderByPriceDescTest() {
        this.createItemList();
        List<Item> itemList = itemRepository.findByPriceLessThanOrderByPriceDesc(10005);
        for (Item item: itemList
             ) {
            System.out.println(item.toString());
        }
    }

    @Test
    @DisplayName("@Query를 이용한 상품 조회 테스트")
    public void findByItemDetailTest() {
        this.createItemList();
        List<Item> itemList = itemRepository.findByItemDetail("테스트 상품 상세 설명");
        for (Item item: itemList
             ) {
            System.out.println(item.toString());
        }
    }

    @Test
    @DisplayName("Querydsl 조회 테스트1")
    public void queryDslTest() {
        this.createItemList();
        // JPAQueryFactory를 이용하여 쿼리를 동적으로 생성합니다.
        // 생성자의 파라미터로는 EntityManager 객체를 넣어줍니다.
        JPAQueryFactory queryFactory = new JPAQueryFactory(em);
        // Querydsl을 통해 쿼리를 생성하기 위해 플러그인을 통해 자동으로 생성된 QItem 객체를 이용합니다.
        QItem qItem = QItem.item;
        // 자바 소스코드지만 SQL문과 비슷하게 소스를 작성할 수 있습니다.
        JPAQuery<Item> query = queryFactory.selectFrom(qItem)
                .where(qItem.itemSellStatus.eq(ItemSellStatus.SELL))
                .where(qItem.itemDetail.like("%" + "테스트 상품 상세 설명" + "%"))
                .orderBy(qItem.price.desc());

        // JPAQuery 메소드 중 하나인 fetch를 이용해서 쿼리 결과를 리스트로 반환합니다.
        // fetch() 메소드 실행 시점에 쿼리문이 실행됩니다.
        List<Item> itemList = query.fetch();

        for (Item item: itemList
             ) {
            System.out.println(item.toString());
        }
    }

    // 상품 데이터를 만드는 새로운 메소드를 하나 만듭니다.
    // 1번부터 5번 상품은 상품의 판매상태를 SELL(판매 중)으로 지정하고
    // 6번부터 10번까지는 판매상태를 SOLD_OUT(품절)로 세팅해 생성합니다.
    public void createItemList2() {
        for (int i = 1; i < 5; i++) {
            Item item = new Item();
            item.setItemNm("테스트 상품" + i);
            item.setPrice(10000 + i);
            item.setItemDetail("테스트 상품 상세 설명 " +i);
            item.setItemSellStatus(ItemSellStatus.SELL);
            item.setStockNumber(100);
            item.setRegTime(LocalDateTime.now());
            item.setUpdateTime(LocalDateTime.now());
            itemRepository.save(item);
        }

        for (int i = 6; i < 10; i++) {
            Item item = new Item();
            item.setItemNm("테스트 상품" + i);
            item.setPrice(10000 + i);
            item.setItemDetail("테스트 상품 상세 설명 " +i);
            item.setItemSellStatus(ItemSellStatus.SOLD_OUT);
            item.setStockNumber(0);
            item.setRegTime(LocalDateTime.now());
            item.setUpdateTime(LocalDateTime.now());
            itemRepository.save(item);
        }
    }


    @Test
    @DisplayName("상품 Querydsl 조회 테스트2")
    public void queryDslTest2() {
        this.createItemList2();

        // BooleanBuilder는 쿼리에 들어갈 조건을 만들어주는 빌더라고 생각하면 됩니다.
        // Predicate를 구현하고 있으며 메소드 체인 형식으로 사용할 수 있습니다.
        BooleanBuilder booleanBuilder = new BooleanBuilder();
        QItem item = QItem.item;
        String itemDetail = "테스트 상품 상세 설명";
        int price = 10003;
        String itemSellStat ="SELL";

        // 필요한 상품을 조회하는데 필요한 "and" 조건을 추가하고 있습니다.
        // 아래 소스에서 상품의 판매상태가 SELL일 때만 booleanBuilder에 판매상태 조건을 동적으로 추가흔 것을 볼수 있다.
        booleanBuilder.and(item.itemDetail.like("%" + itemDetail +"%"));
        booleanBuilder.and(item.price.gt(price));

        if(StringUtils.equals(itemSellStat, ItemSellStatus.SELL)) {
            booleanBuilder.and(item.itemSellStatus.eq(ItemSellStatus.SELL));
        }

        // 데이터를 페이징해 조회하도록 PageRequest.of() 메소드를 이용해 Pageble 객체를 생성
        // 첫 번째 인자는 조회활 페이지 정보, 두 번째 인자는 한 페이지당 조회할 데이터 개수를 넣어줍니다.
        Pageable pageable = PageRequest.of(0, 5);

        // QueryDslPredicateExecutor 인터페이스에서 정의한 findAll() 메소드를 이용해 조건에 맞는 데이터를
        // Page 객체로 받아옵니다.
        Page<Item> itemPagingResult = itemRepository.findAll(booleanBuilder, pageable);
        System.out.println("total elements : " + itemPagingResult.getTotalElements());

        List<Item> resultItemList = itemPagingResult.getContent();

        for (Item resultItem: resultItemList
             ) {
            System.out.println(resultItem.toString());
        }
    }
}

service

package com.example.shopping_.service;

import com.example.shopping_.entity.Member;
import com.example.shopping_.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.thymeleaf.util.Validate;

@Service
// 비즈니스 로직을 담당하는 서비스 계층 클래스에
// @Transactional 어노테이션을 선언합니다.
// 로직을 처리하다가 에러가 발생하면
// 변경된 데이터 로직을 처리하기 전으로 콜백해줍니다.
@Transactional
// 빈 주입 방법중 한 개인데
// @NonNull 이나 final 붙은 필드에 생성자를 생성
@RequiredArgsConstructor
// MemberService가 UserDetailsService를 구현합니다.
public class MemberService implements UserDetailsService {

    // 생성자가 1개이므로 @Autowired를 생략 가능
    private final MemberRepository memberRepository;

    public Member saveMember(Member member) {
        validateDuplicateMember(member);
        return memberRepository.save(member);
    }

    // saveMember 메소드가 실행될 때 member 매개변수가 넘어오면
    // validateDuplicateMember(Member member)가 실행되고
    // 매개변수를 받아와서 레포지토리에서 만든 findByEmail을 실행해서
    // 이메일이 있는지 검사하고 findMember에 넣어줍니다.
    // 그리고 빈값이 아니면 IllegalStateException 예외를 발생시켜줍니다.
    private void validateDuplicateMember(Member member) {
        Member findMember = memberRepository.findByEmail(member.getEmail());
        if (findMember != null) {
            throw new IllegalStateException("이미 가입된 회원입니다.");
        }
    }

    // UserDetailsService 인터페이스의 loadUserByUsernmae() 메소드를 오버라이딩합니다.
    // 로그인할 유저의 email을 파라미터로 전달 받습니다.
    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        Member member = memberRepository.findByEmail(email);

        if(member == null) {
            throw new UsernameNotFoundException(email);
        }

        // UserDetail을 구현하고 있는 User 객체를 반환해줍니다.
        // User 객체를 생성하기 위해서 생성자로 회원의 메일, 비밀번호, role을 파라미터로 넘겨줌
        return User.builder()
                .username(member.getEmail())
                .password(member.getPassword())
                .roles(member.getRole().toString())
                .build();
    }
}
package com.example.shopping_.service;

import com.example.shopping_.DTO.MemberFormDTO;
import com.example.shopping_.entity.Member;
import org.assertj.core.api.Assertions;
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 org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.context.TestPropertySource;
import org.springframework.transaction.annotation.Transactional;
import static org.junit.jupiter.api.Assertions.assertThrows;

@SpringBootTest
// 테스트 실행 후 롤백 처리를 해줘서 반복적으로 테스트 가능
@Transactional
@TestPropertySource(locations = "classpath:application-test.properties")
public class MemberServiceTest {

    @Autowired
    MemberService memberService;

    @Autowired
    PasswordEncoder passwordEncoder;

    // 회원 정보를 입력한 Member 엔티티를 만드는 메소드를 작성
    public Member createMember() {
        MemberFormDTO memberFormDTO = new MemberFormDTO();
        memberFormDTO.setEmail("test@naver.com");
        memberFormDTO.setName("테스터");
        memberFormDTO.setAddr("서울시 마포구 합정동");
        memberFormDTO.setPassword("1234");
        return Member.createMember(memberFormDTO, passwordEncoder);
    }

    @Test
    @DisplayName("회원가입 테스트")
    public void saveMemberTest() {
        Member member = createMember();
        Member saveMember = memberService.saveMember(member);

        Assertions.assertThat(member.getEmail()).isEqualTo(saveMember.getEmail());
        Assertions.assertThat(member.getName()).isEqualTo(saveMember.getName());
        Assertions.assertThat(member.getAddr()).isEqualTo(saveMember.getAddr());
        Assertions.assertThat(member.getPassword()).isEqualTo(saveMember.getPassword());
        Assertions.assertThat(member.getRole()).isEqualTo(saveMember.getRole());
    }

    @Test
    @DisplayName("중복 회원 가입 테스트")
    public void saveDuplicateMemberTest() {
        Member member1 = createMember();
        Member member2 = createMember();
        memberService.saveMember(member1);

        Throwable e= assertThrows(IllegalStateException.class, () -> {
            memberService.saveMember(member2);
        });

        Assertions.assertThat("이미 가입된 회원입니다.").isEqualTo(e.getMessage());
    }
}

JaCoCo를 활용한 테스트 커버리지 확인

plugins {
    id 'java'
    id 'org.springframework.boot' version '2.7.8'
    id 'io.spring.dependency-management' version '1.0.15.RELEASE'
    id 'jacoco'
}

jacoco {
    // JaCoCo 버전
    toolVersion = '0.8.7'

//  테스트결과 리포트를 저장할 경로 변경하는 방법
//  default는 "$/jacoco"
//  reportsDir = file("$buildDir/customJacocoReportDir")
    reportsDirectory = layout.buildDirectory.dir('customJacocoReportDir')
}

test {
    // finalizedBy : 이(test) 작업에 대해 주어진 종료자 작업을 추가
    finalizedBy jacocoTestReport // test 작업이 끝나고 jacocoTestReport를 실행
}
jacocoTestReport {
    // dependsOn : 이 작업에 지정된 종속성을 추가
    dependsOn test // jacocoTestReport 에 test라는 종속성을 추가
}
jacocoTestReport {
    reports {
        xml.required = false
        csv.required = false
        html.outputLocation = layout.buildDirectory.dir('jacocoHtml')
    }
}
jacocoTestCoverageVerification {
    violationRules {
        rule {
            limit {
                minimum = 0.5
            }
        }

        rule {
            enabled = false
            element = 'CLASS'
            includes = ['org.gradle.*']

            limit {
                counter = 'METHOD'
                value = 'COVEREDRATIO'
                minimum = 0.5
            }
        }
    }
}

JaCoCo 플러그인을 적용

plugins {
    id 'jacoco'
}

이 플러그인을 적용하면 jacocoTestReport가 생성

알아두어야 할 사항이 있습니다. 바로 test가 먼저 실행된 뒤에 task가 실행되어야 한다는 점입니다.

실행순서 : test -> jacocoTestReport -> jacocoTestCoverageVerification

test {
    // finalizedBy : 이(test) 작업에 대해 주어진 종료자 작업을 추가
    finalizedBy jacocoTestReport // test 작업이 끝나고 jacocoTestReport를 실행
}
jacocoTestReport { 
    // dependsOn : 이 작업에 지정된 종속성을 추가
    dependsOn test // jacocoTestReport 에 test라는 종속성을 추가
}

JaCoCo 플러그인 구성

jacoco {
    toolVersion = "0.8.7"
    reportsDirectory = layout.buildDirectory.dir('customJacocoReportDir')
}
  • toolVersion : Jacoco의 jar 버전 설정입니다.
  • reportsDir : Jacoco report 결과물 디렉토리 설정입니다.

다음으론 report의 결과물을 설정하는 작업입니다. xml, csv, html 으로 설정할 수 있습니다.

jacocoTestReport {
    reports {
        xml.required = false
        csv.required = false
        html.outputLocation = layout.buildDirectory.dir('jacocoHtml')
    }
}
  • required : 이전엔 enabled로 쓰였던 속성으로 생성 여부를 결정하는 플래그입니다.
  • outputLocation : 보고서를 생성할 파일 시스템의 위치입니다.

기본설정이기도 하지만 보기 쉬운 html로 report를 보는 것으로 설정하였습니다. 원하시는 파일이 있다면 false로 되어있는 값을 true로 변경하시면 됩니다.

Code Coverage 측정 항목 정의

JacocoCoverageVerification 는 코드 커버리지 측정을 구성 규칙에 따라 충족되는 경우를 확인하는 데 사용할 수 있습니다. violationRules 을 통해 rule 을 정의할 수 있습니다. 정의된 rule 중 하나라도 충족되지 않으면 build가 실패합니다.

jacocoTestCoverageVerification {
    violationRules {
        rule {
            limit {
                minimum = 0.5
            }
        }

        rule {
            enabled = false
            element = 'CLASS'
            includes = ['org.gradle.*']

            limit {
                counter = 'METHOD'
                value = 'COVEREDRATIO'
                minimum = 0.5
            }
        }
    }
}
  • rule : 룰을 정의합니다.
  • enabled : true 로 설정하면 포함된 작업에 대한 데이터가 수집됩니다.
  • element : Java 요소의 유형을 나타냅니다. (BUNDLE, CLASS, GROUP, METHOD, PACKAGE, SOURCEFILE)
  • limit : rule 에 대한 제한을 설정합니다.
    • counter : 커버리지 측정 단위를 설정합니다. (BRANCH, CLASS, COMPLEXITY, INSTRUCTION, LINE, METHOD)
    • minimum : 제한에 대한 최소 예상값을 설정합니다. counter 값을 value 에 맞게 표현했을 때 최솟값을 말합니다. 이 값을 통해 jacocoTestCoverageVerification 의 성공 여부가 결정됩니다.
    • maximum : 제한에 대한 최대 예상값을 설정합니다.
    • value : 측정한 counter의 정보를 어떠한 방식으로 보여줄지 정합니다. (TOTALCOUNT, COVEREDCOUNT, MISSEDCOUNT, COVEREDRATIO,MISSEDRATIO )
  • includes : 검사에 포함되어야 하는 요소 목록입니다. 비워두면 모든 요소가 포함됩니다. 기본값은 [*]입니다.

여기서 test를 더블 클릭하면 build에 reports가 생기고 tests 밑에 test가 생기고 index가 생깁니다. 여기서 index를 키고 열면 다음과 같은 창이 나옵니다.

작성한 Test 코드의 성공률과 수행정보를 볼 수 있다.

'jacocoTestReport'를 더블클릭하면, 프로젝트 폴더의 build->reports->jacoco 폴더가 생성됨

reports->jacoco->test->html->index.html 을 브라우저로 실행하면, 작성한 Test 코드의 코드커버리지를 확인 할 수 있음.

연습

package com.example.jococo;


public class Coco {
    public String hello(String name) {
        switch (name) {
            case "hi":
                return "nice to meet you";
            case  "Minju":
                return "Kim";
            default:
                return "hello method";
        }
    }

    public String callFoo() {
        return "call Foo Class";
    }
}

test 돌려보기

package com.example.jococo;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

class CocoTest {

    @DisplayName("testWithMinju")
    @Test
    public void fooTest() {
        Coco coo = new Coco();
        String args = "Minju";

        String ret = coo.hello(args);
        // assertEquals :  두 객체의 값이 같은지 여부
        assertEquals("Kim", ret);
    }

}


빌더 단위 테스트

package kr.pe.playdata.domain;

import org.junit.jupiter.api.Test;

public class LombokPerTest {
    @Test
    public void 빌더_테스트_2() {
    	
        //given
		String name = "jueun";
		int age = 24;
		String grade = "4";
        
        //when
		LombokPerson p1 = LombokPerson.builder()
		        .name(name)
		        .age(age)
		        .grade(grade)
		        .build();
                
        //then
        assertThat(p1.getGrade()).isEqualTo(grade);
        assertThat(p1.getAge()).isEqualTo(age);
        assertThat(p1.getName()).isEqualTo(name);
                
    }
}
package com.example.rest_book1.domain;

import lombok.extern.log4j.Log4j2;
import org.aspectj.lang.annotation.After;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import java.time.LocalDateTime;
import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

@ExtendWith(SpringExtension.class)
@SpringBootTest
@Log4j2
class PostsRepositoryTest {


    @Autowired
    PostsRepository postsRepository;

    @AfterEach
    public void cleanUp() {
        postsRepository.deleteAll();
    }

    @Test
    @DisplayName("게시글 저장")
    public void saveBoardTest() {
        // given
        String title = "테스트 게시글";
        String content = "테스트 본문";

        // 테이블에 posts에 insert/update 쿼리를 실행합니다.
        // id 값이 있다면 update, 없다면 insert 쿼리가 실행됩니다.
        postsRepository.save(Posts.builder()
                .title(title)
                .content(content)
                .author("tester@naver.com")
                .build());

        // when
        // 테이블 posts에 있는 모든 데이터를 조회해오는 메소드입니다.
        List<Posts> postsList = postsRepository.findAll();

        // then
        Posts posts = postsList.get(0);
        log.info("게시물 보기 : " +posts);
        Assertions.assertThat(posts.getTitle()).isEqualTo(title);
        Assertions.assertThat(posts.getContent()).isEqualTo(content);
    }

    @Test
    public void BaseTimeEntityTest() {
        // given
        LocalDateTime now = LocalDateTime.of(2022,3, 7,6,51);
        postsRepository.save(Posts.builder()
                        .title("title")
                        .content("content")
                        .author("author")
                .build());

        // when
        List<Posts> postsList = postsRepository.findAll();

        // then
        Posts posts = postsList.get(0);

        log.info(">>>>>>>>> createDate = "
                + posts.getCreatedDate()
                + ", modifiedDate = "
                + posts.getModifiedDate());

        // isAfter : 날짜 비교
        Assertions.assertThat(posts.getCreatedDate()).isAfter(now);
        Assertions.assertThat(posts.getModifiedDate()).isAfter(now);
    }
}

PostsRepositoryTest

package com.example.rest_book1.domain;

import lombok.extern.log4j.Log4j2;
import org.aspectj.lang.annotation.After;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import java.time.LocalDateTime;
import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

@ExtendWith(SpringExtension.class)
@SpringBootTest
@Log4j2
class PostsRepositoryTest {


    @Autowired
    PostsRepository postsRepository;

    @AfterEach
    public void cleanUp() {
        postsRepository.deleteAll();
    }

    @Test
    @DisplayName("게시글 저장")
    public void saveBoardTest() {
        // given
        String title = "테스트 게시글";
        String content = "테스트 본문";

        // 테이블에 posts에 insert/update 쿼리를 실행합니다.
        // id 값이 있다면 update, 없다면 insert 쿼리가 실행됩니다.
        postsRepository.save(Posts.builder()
                .title(title)
                .content(content)
                .author("tester@naver.com")
                .build());

        // when
        // 테이블 posts에 있는 모든 데이터를 조회해오는 메소드입니다.
        List<Posts> postsList = postsRepository.findAll();

        // then
        Posts posts = postsList.get(0);
        log.info("게시물 보기 : " +posts);
        Assertions.assertThat(posts.getTitle()).isEqualTo(title);
        Assertions.assertThat(posts.getContent()).isEqualTo(content);
    }

    @Test
    public void BaseTimeEntityTest() {
        // given
        LocalDateTime now = LocalDateTime.of(2022,3, 7,6,51);
        postsRepository.save(Posts.builder()
                        .title("title")
                        .content("content")
                        .author("author")
                .build());

        // when
        List<Posts> postsList = postsRepository.findAll();

        // then
        Posts posts = postsList.get(0);

        log.info(">>>>>>>>> createDate = "
                + posts.getCreatedDate()
                + ", modifiedDate = "
                + posts.getModifiedDate());

        // isAfter : 날짜 비교
        Assertions.assertThat(posts.getCreatedDate()).isAfter(now);
        Assertions.assertThat(posts.getModifiedDate()).isAfter(now);
    }
}
profile
발전하기 위한 공부

0개의 댓글