테스트 커버리지 100% 향해서

테스트 커버리지 100% 향해서

도입

테스팅이라는 것의 중요성을 알고 있었으나, 해야하는데 해야하는데 미뤄지는 숙제와도 같았다.

어떻게 테스팅을 해야하는지 고민도 많았고 개념도 많이 부족하였었다.

마침 지난 학기에 테스팅 관련 수업이 신설되었었고, 이를 들어보았다.

테스팅에 관한 여러 개념들을 많이 배울 수 있었고 여러 유용한 점도 많이 알 수 있었다.

특히 커버리지라는 개념을 배우고 나서 토스 컨퍼런스인 slash 21에서 이 주제로 발표를 한 것을 보았다.

인스트럭션 커버리지 100% 기준을 달성하기 위해 여러 이슈들을 어떻게 해결했는지 알려주는 좋은 발표였다.

이후 더 찾아보고 이 테스트 커버리지는 현업에서 충분히 많이 사용되는 것으로 보였고 굉장히 효과적이라는 생각이 들었다.

또한 학교 프로젝트에서 서버를 구축하면서 매번 수동으로 swagger ui를 가지고 테스팅하는 것은 매우 비효율적이라고 생각이 들었다.

API의 반환타입이 바뀌거나 하면 또 매번 해줘야하며, session을 통해 구축하는 거면 로그인까지 해줘야하는 문제점이 있었다.

따라서 학교 프로젝트의 서버에 test code를 추가하여 BRANCH 기준으로 100% 달성하였다.

이는 추후 몇가지 기능을 더 추가할 때 매우 큰 도움이 될 것이다.

어떻게 커버리지를 달성했는지 알 수 있는 jacoco 플러그인과

컨트롤러, 리포지토리, 서비스, 도메인 등을 어떻게 테스트 코드를 작성했는지 포스팅해보겠다.

테스트 커버리지


테스트 케이스가 얼마나 코드를 커버하고 있는지 나타내는 지표

테스트 커버리지는 테스트가 어느정도 코드를 테스트하고 있는지 수치화하기 때문에,

테스트를 언제 얼마만큼 작성해야하는지 기준이 된다는 큰 장점이 있다.

다음과 같이 여러개의 coverage들이 있지만 위의 두 개만 알아보자.

  1. statement(line) coverage

  2. decision(branch) coverage

  3. condition/decision coverage

  4. modified condition/decision coverage

line coverage

코드의 각 라인이 한번이라도 실행되었는지 측정

테스트 케이스들이 모든 라인을 한번이라도 실행한다면, line coverage 100% 라고 할 수 있다.

예를 들어 설명해보면 아래의 경우 test case가 num=11이면 #1, #2, #3 모두 실행하게 되어

100% line coverage를 달성하게 된다.

private void printNumber(int num){
    if(num > 10){ // #1 
        System.out.println("bigger than 10"); // #2
    } 
    System.out.println(num); // #3
}

branch coverage

분기문(if-else, switch)의 조건식의 결과가 true, false를 한번이라도 충족했는지 측정.

테스트 케이스들이 모든 분기문들의 조건식의 결과가 true, false를 한번이라도 충족하게 한다면, branch coverage 100% 라고 할 수 있다.

위의 예제에서 num=11이라는 테스트 케이스는 조건식 num > 10 이 true인 경우만 만족시키므로,

branch coverage 100% 달성하기 위해서는 num=11, num=10 이렇게 2가지의 테스트 케이스가 필요하다.

참고로 num1 > 10 && num2 > 5 이런 조건식의 전체 결과가 true, false인지만 따지는게 branch coverage이며,

num > 10num2 > 5 각각 모두 true, false인지 따지는 것은 condition/decision coverage 나

modified condition / decision coverage 등에서 수행한다.

이보다 더 강력한 multiple condition coverage, all path coverage 등이 있지만,

이는 매우 강력한만큼 매우 많은 노력이 들기 때문에 충분히 효과적이면서 쉬운 line coverage나 branch coverage 등이 많이 쓰인다.

이번 프로젝트에서는 line coverage 100%를 달성해보려 한다.

jacoco 설치


jacoco는 java 코드의 커버리지를 측정하는 라이브러리로, html, csv, xml과 같은 형태로 리포트(측정 결과)를 생성해준다.

이 jacoco 플러그인을 통해 테스트 커버리지 측정을 해볼 것이다.

build.gradle에서 추가하면 된다.

build.gradle에서 plugins 부분에 아래와 같이 추가한다.

plugins {
    ...
    id 'jacoco' // 추가
}

그리고 아래와 같이 테스크를 추가해주자.

jacocoTestReport{
   reports{
       html.enabled true // html 형식으로 볼 수 있다.
       xml.enabled false
       csv.enabled false
   }
   
   // 아래 afterEvaluate 부분은 리포트를 보여줄 때, 제외하고 싶은 것을 의미한다.
   // 현재 프로젝트에서 있는 QueryDSL과 룸북의 Builder 어노테이션이 만들어주는 코드를 
   // 테스트하는 것은 의미없으므로 제외해준다. 
    afterEvaluate {
        classDirectories.setFrom(files(classDirectories.files.collect{
            fileTree(dir: it, excludes: [
                    '**/aggregate/Q*', // QueryDSL이 생성하는 엔티티를 제외한다.
                    '**/*Builder*' // 룸북을 통해 생성된 빌더를 제외한다.
            ])
        }))
    }

    finalizedBy 'jacocoTestCoverageVerification' // 작업 후 여기 있는 task를 실행한다. 
}

그리고 아래는 어떤 커버리지를 얼마만큼 달성할 것인지 나타내는 테스크이다. 이를 추가해주자.

jacocoTestCoverageVerification{
    violationRules{
        rule{
            element = 'CLASS'
            // 위에서 제외해준 것은 리포트를 표시할 때만 안보여주는 것이므로, 
            // 위에서 제외했어도 검증은 하게 된다.
            // 빌드 시 검증에 실패하게 되면 빌드가 되지 않으므로 다음과 같이 작성해주자.
            excludes = [
                    '**.aggregate.Q*',
                    '**.*Builder*',
            ]
            limit{
                counter = 'LINE' // 라인 기준이며, 이 외에도 BRANCH, CLASS, METHOD, INSTRUCTION이 있다.
                value = 'COVEREDRATIO' // 리포트의 커버리지 달성 정도를 %로 표기.
                minimum = 1.00 // 최소 커버리지 달성 요구 조건. 여기서는 100%를 의미.
            }
        }
    }
}

그리고 마지막으로 test task를 다음과 같이 작성해준다.

test {
    useJUnitPlatform()
    finalizedBy 'jacocoTestReport'
}

이렇게 되면 test task를 수행하면 test -> jacocoTestReport -> jacocoTestCoverageVerification 순으로 진행된다.

이렇게 작성한 후 ./gradlew test를 실행시키면 다음과 같은 화면을 볼 수 있다.

jacocoTestCoverageVerification task가 실패했음을 볼 수 있으며 각 부분이 얼마만큼 달성했는지 보여준다.

좀 더 쉽게 눈으로 확인할 수 있는데 build/reports/jacoco/test/html/index.html를 통해 확인 할 수 있다.

intellij 를 이용하면 옆에 뜨는 웹브라우저 아이콘을 클릭해서 볼 수 있다.

이렇게 한눈에 얼마만큼 달성했는지 볼 수 있다.

포스팅을 하기 전 borrow 패키지는 라인 기준으로 100% 달성하도록 테스트 케이스를 작성했었다.

그에 따라 초록색 게이지가 꽉 찬 것을 볼 수 있다.

이제 어떻게 라인 커버리지 100%를 달성하는지 경험을 써보겠다.

기본 Rule


먼저 각 레이어별 테스트 전략을 쓰기 전에, test 작성 시 기본 패턴과 규칙을 알아보자

given - when - then 패턴

아래는 하나의 테스트 함수를 작성할 때 다음과 같이 작성하는 것이다.

  • given(준비): 어떠한 데이터가 준비되었을 때 (Mock 객체의 행동도 정의)

  • when(실행): 어떠한 함수를 실행하면

  • then(검증): 어떠한 결과가 나와야 한다.

이를 코드로 보면 다음과 같다.

@Test
@DisplayName("이미 반납되었을 때 반납 실패")
public void testReturnFailedWhenAlreadyReturned(){
    //given
    final List<Borrow> returnedList = returnedList();
    doReturn(returnedList).when(borrowRepository).findBorrowsByBookId(isA(Long.class));

    //when
    IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,()->returnService.returnBook(1L));

    //then
    assertEquals(exception.getMessage(), "이미 반납되었습니다.");
}

너무 자세히 볼 필요는 없으며, 아래에서 given - when - then을 자세히 포스팅하겠다.

FIRST 규칙

클린 코드 책의 깨끗한 테스트코드 5가지 규칙을 의미하며, 앞 글자를 따서 FIRST라고 한다.

  1. Fast: 테스트는 빠르게 동작하여 자주 돌릴 수 있어야 한다.

    • 스프링의 모든 빈을 올려놓고 하는 테스트는 꼭 필요할 때만 하자.

    • Mockito 라이브러리를 적극적으로 이용하자.

    • 토스에서는 Mockito의 Mock 객체 생성시간까지 줄일 수 있도록 직접 Mock 객체를 구현하였다.

  2. Independent: 각각의 테스트는 독립적이며 서로 의존해서는 안된다.

    • 데이터는 각 테스트마다 공유되지 않도록 하자.

    • 테스트의 순서가 매번 달라질 수 있는데 순서에 따라 실패하지 않도록 하자.

  3. Repeatable: 어느 환경에서도 반복 가능해야 한다.

    • 테스트 환경을 구축해놓고 테스트를 작성하자.

    • 리포지터리 테스트 시에는 @DataJpaTest 어노테이션을 사용해주자. @Transactional 어노테이션을 포함하고 있어 테스트가 끝나면 롤백을 시켜준다.

  4. Self-Validating: 테스트는 성공 또는 실패로 bool 값으로 결과를 내어 자체적으로 검증되어야 한다.

    • Assertions의 assert 함수들을 then에서 호출하자.
  5. Timely: 테스트는 적시에 즉, 테스트하려는 실제 코드를 구현하기 직전에 구현해야 한다

    • 이번에는 이미 코드 구현이 되어있어 만족하지 못하였다.

    • 추후 새로운 기능을 추가시에, TDD로 개발한다면 자동으로 만족할 것이다.

Mock

테스트하려는 클래스가 다른 특정 클래스에 의존하고 있을 때, 의존하는 객체를 실제로 사용하는 것이 아니라, 미리 지정한 행동을 하는 가짜 객체로 대체하여 사용한다. 이 때 사용한 가짜 객체를 Mock 객체라고 한다.

우리는 Mockito library를 통해 쉽게 Mock 객체를 구현할 것이다.

몇 가지 사용법을 알아보자.

@InjectMocks, @Mock

아래처럼 작성하면 UserRepository의 Mock 객체를 UserService에 주입시켜준다.

class TestClass{
    @InjectMocks
    private UserService userService;
    
    @Mock
    private UserRepository userRepository;
}

stubbing

Mock 객체의 어떤 메서드를 호출했을 때 미리 준비된 객체를 반환하는 것.

doReturn(List.of(new User("honggildong"))).when(userRepository).getUsers();

userRepositorygetUsers() 함수를 호출하면 이름이 ‘honggildong’인 유저가 한명 있는 리스트가 반환된다.

각 레이어별 테스트 전략

프리젠테이션(컨트롤러)

여기서 한 번 컨트롤러의 역할에 대해 생각해보자.

프리젠테이션 계층(컨트롤러) 는 사용자의 요청을 받아 응용 영역에 전달하고 응용 영역의 처리 결과를 사용자에게 전달하는 역할을 한다.

즉 프리젠테이션 계층에서 확인해야하는 것은 다음과 같이 생각할 수 있다.

  1. 사용자의 요청을 정확하게 받았는지

  2. 서비스 계층에 잘 전달해줬는지

  3. 서비스 계층의 처리 결과를 사용자에게 잘 전달했는지

따라서 여기서 컨트롤러가 의존하고 있는 서비스들을 Mock 객체로 구현하여도 무방하다고 볼 수 있다.

또 Mock을 사용함으로 스프링 컨테이너를 사용하지 않으므로 FIRSTFast 또한 충족할 수 있다.

테스트하려는 컨트롤러는 다음과 같다. borrow(borrowRequest)메서드를 테스트해보자.

@RestController
@RequiredArgsConstructor
public class BorrowController {
    private final BorrowService borrowService;
    
    ...

    @PostMapping(value = "/api/borrow", produces = "application/json; charset=utf8")
    public Borrow borrow(@RequestBody @Valid BorrowRequest borrowRequest){
       return borrowService.borrowBook(borrowRequest);
    }
    
   ...
}

테스트 코드는 다음과 같이 작성하자.

@ExtendWith(MockitoExtension.class) // junit5에서 Mockito library 사용하기 위해 추가해줘야함.
class BorrowControllerTest{
   @InjectMocks // Mock 객체들을 주입
   private BorrowController borrowController;

   @Mock // Mock 객체
   private BorrowService borrowService;
   
   ...

   private MockMvc mockMvc; // MockMvc를 통해 API 요청을 할 수 있도록, Spring test에서 이를 제공.

   private Gson gson; // json 파싱을 위해 사용.

   @BeforeEach // 매 테스트가 시작되기 전 실행됨.
   public void init(){
      mockMvc = MockMvcBuilders.standaloneSetup(borrowController).build();
      gson = new Gson();
   }

   @Test
   @DisplayName("대여하기 성공") // 화면에 표시되는 문구.
   void testBorrowSuccess() throws Exception {
      //given
      final BorrowRequest borrowRequestDto = BorrowRequest.builder()
              .borrowerId(1L).borrowerName("hongildong").bookId(2L).build();
      final Borrow borrow = Borrow.builder().state(BorrowState.BORROWING).build();

      doReturn(borrow).when(borrowService).borrowBook(argThat(b -> b.getBorrowerId() == 1));

      //when
      final ResultActions resultActions = mockMvc.perform(
              MockMvcRequestBuilders.post("/api/borrow")
                      .contentType(MediaType.APPLICATION_JSON)
                      .content(gson.toJson(borrowRequestDto))
      );

      //then
      final MvcResult mvcResult = resultActions.andExpect(status().isOk()).andReturn();
      assertEquals(mvcResult.getResponse().getStatus(), 200);
      final Borrow response = gson.fromJson(mvcResult.getResponse().getContentAsString(), Borrow.class);
      assertEquals(response.getState(), BorrowState.BORROWING);
   }
   
   ...
}

testBorrowSuccess()given-when-then으로 나눠서 살펴보자.

given

먼저 요청을 하기위한 Dto인 BorrowRequestBorrowService의 반환값으로 borrow를 준비하였다.

그리고 컨트롤러가 서비스에게 제대로 데이터를 넘겼는지 확인하기 위해

argThat(b -> b.getBorrowId == 1)을 통해서 해당 값에 일치하는 요청이 들어왔을 때만 값을 반환하도록 하였다.

when

MockMVC를 통해 API 요청을 수행하였다. /api/borrow URL로 POST 요청을 하였으며, 타입은 JSON 그리고 미리 생성한 요청 Dto 객체를 Gson을 이용해 json으로 만들어 보내었다.

then

결과를 검증하는 단계로, 호출한 API의 status code가 200인지 확인하였고

정상적으로 데이터를 받았는지 검증하였다.

테스트를 돌려보면 다음과 같이 결과가 나온다.

응용 계층 서비스

응용 계층의 경우도 프리젠테이션 계층(컨트롤러) 와 비슷하게 테스트하였다.

의존하는 Repository를 Mock 객체로 하고 Stubbing을 해주었다.

아래는 테스트하려는 서비스이다.

@Service
@Slf4j
@RequiredArgsConstructor
public class BorrowSearchService {
    private final BorrowRepository borrowRepository;
    private final UserRepository userRepository;

    ...
    
    // below is for one user
    @Transactional
    public List<BorrowBookResponse> getMyBorrowings(Long userId){
        checkUserExist(userId);
        return borrowRepository.findBorrowbookAllByBorrower_UserIdAndState(userId, BorrowState.BORROWING);
    }

    ...

    private void checkUserExist(Long userId){
        if(!userRepository.existsById(userId)) 
            throw new IllegalArgumentException("해당 유저가 없습니다.");
    }
}

컨트롤러의 예제와는 다르게 실패하는 경우에 대해 작성해보자.

@ExtendWith(MockitoExtension.class)
class BorrowSearchServiceTest {

   @InjectMocks
   private BorrowSearchService borrowSearchService;

   @Mock
   private BorrowRepository borrowRepository;
   @Mock
   private UserRepository userRepository;
    
    ...

   @Test
   @DisplayName("유저의 현재 대여중인 목록 가져오기 실패")
   public void testGetOneUserBorrowingsFailed() {
      // given
      doReturn(false).when(userRepository).existsById(-1L);

      // when
      final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class
              , () -> borrowSearchService.getMyBorrowings(-1L));

      // then
      assertEquals(exception.getMessage(), "해당 유저가 없습니다.");
   }
   ...
}

BorrowSerchServicegetMyBorrowings(int) 함수를 호출하면 해당 사용자의 대여중인 목록을 반환해준다.

그리고 해당 사용자가 없으면 실패하게 되어있는데, 이 경우를 given-when-then으로 알아보자.

given

userRepository의 exsistsById()가 호출되면 false를 반환하게 된다.

이렇게 하면 실패하여 예외를 던지게 될 것이다.

when

실제 함수를 호출하고 IllegalArgumentException 타입으로 exception을 반환받는다.

then

예외의 메시지가 해당 유저가 없습니다. 와 일치하는지 확인해본다.

도메인 (엔티티)

도메인 엔티티는 도메인 로직을 책임진다.

테스트하려는 엔티티의 코드는 다음과 같다.

public class Borrow {

   ...
   
   @Enumerated(EnumType.STRING)
   private BorrowState state;

   @Column(name = "expired_at")
   private LocalDate expiredAt;
   
   ...

   public void expired() {
      if (state == BorrowState.RETURNED) throw new IllegalStateException("이미 반납이 완료되었습니다.");
      if (LocalDate.now().isBefore(expiredAt)) throw new IllegalStateException("아직 연체 기간이 남았습니다.");
      state = BorrowState.EXPIRED;
   }
   ...
} 

여기서 연체로 변경한 것을 성공했는지 테스트해보자.

class BorrowTest {

   @Test
   @DisplayName("연체로 변경 성공")
   public void testSetExpiredSuccess() {
      //given
      Borrow borrow = Borrow.builder().state(BorrowState.BORROWIN)
              .expiredAt(LocalDate.now().minusDays(1)).build();

      //when
      borrow.expired();

      //then
      assertEquals(borrow.getState(), BorrowState.EXPIRED);
   }
   
   ...
}

여기서 연체로 바꾼 뒤, 상태가 EXPIRED로 바뀌었는지 확인하고 있다.

given

BORROWING 상태의 Borrow 객체를 생성하고 ExpiredAt을 현재보다 하루 전으로 설정한다.

when

expired() 함수를 호출한다.

then

엔티티의 state가 EXPIRED로 바뀌었는지 확인한다.

커버리지를 100% 달성하기 위해서 예외들이 발생하는 테스트 케이스들도 작성해주자.

도메인 (리포지터리)

리포지터리 테스트는 위에서 봤던 것들이랑 좀 다른 점이 있다.

  1. 리포지터리를 Spring Data JPA를 사용하고 있는데, Spring Data JPA가 만들어주는 메서드에 대해서는 테스트 할 필요가 없다. 그 이유는 이미 충분히 검증되어 사용되고 있는 라이브러리이기 때문에 굳이 테스트 할 필요가 없기 때문이다. 테스트할 부분은 N+1 문제를 해결하기 위해 집적 QueryDSL을 작성한 부분이다.

  2. 리포지터리의 경우 실제 데이터가 생성되었는지 삭제되었는지 확인할 필요가 있다. 따라서 테스트용 디비 환경 구축을 할 필요가 있다. 물론 실제 디비 환경에서 테스트할 수도 있다.

먼저 환경 구축부터 해보자.

테스트용 디비 환경 구축

아래 내용을 build.gradle에 추가해주자.

dependencies{
   ...
   testImplementation 'com.h2database:h2' // test시에만 작동
}

그리고 아래 내용을 application.yaml에 추가해주자.

---
spring:
  config:
    activate:
      on-profile: test
  datasource:
    url: jdbc:h2:mem:testdb
    platform: h2
    username: sa
    password:
    initialization-mode: always
  jpa:
    database: h2
    show-sql: true
    hibernate:
      ddl-auto: create
    generate-ddl: true
    database-platform: org.hibernate.dialect.H2Dialect
  h2:
    console:
      enabled: true

이렇게 설치하면 프로필을 test로 했을 때만 이 설정을 사용하게 된다.

테스트 케이스

테스트하려는 코드는 다음과 같다.

@RequiredArgsConstructor
public class BorrowRepositoryImpl implements BorrowRepositoryCustom {
   private final JPAQueryFactory queryFactory;

   @Override
   public List<BorrowBookResponse> findBorrowbookAllByState(BorrowState state) {
      return find(borrow.state.eq(state));
   }
   
   ...

   private List<BorrowBookResponse> find(Predicate predicate) {
      return queryFactory
              .select(Projections.constructor(BorrowBookResponse.class,
                      borrow.id,
                      borrow.state,
                      borrow.createdAt,
                      borrow.expiredAt,
                      borrow.borrower.userId,
                      borrow.borrower.userName,
                      book
              )).from(borrow)
              .join(book).on(borrow.bookId.eq(book.book_id))
              .where(predicate)
              .fetch();
   }
}

위 함수는 BorrowState를 조건으로 검색하고, Book과 join하여 반환한다.

실제 BorrowRepositoryImpl를 사용할 것은 아니고, BorrowRepository 인터페이스를 통해 테스트 할 것이다.

테스트 코드는 다음과 같다.

@ExtendWith(SpringExtension.class) // junit5에서 @DataJpaTest랑 같이 사용하기 위해 필요
@DataJpaTest // JPA와 관련된 객체들만 로딩, 테스트가 끝나면 자동으로 롤백
@Import(TestConfig.class) // QueryDSL 사용에 필요한 빈을 사용하기 위해, 테스트용 Configuration import
@ActiveProfiles("test") // test db 환경인 H2를 사용하기 위한 것
class BorrowRepositoryTest {
   @Autowired
   private BorrowRepository borrowRepository;

   @Autowired
   private BookRepository bookRepository;

   @Test
   @DisplayName("상태로 대여-책 검색 성공")
   public void testFindByStateSuccess(){
      //given

      //when
      List<BorrowBookResponse> borrowBookList1 = borrowRepository
              .findBorrowbookAllByState(BorrowState.BORROWING);
      List<BorrowBookResponse> borrowBookList2 = borrowRepository
              .findBorrowbookAllByState(BorrowState.EXPIRED);
      List<BorrowBookResponse> borrowBookList3 = borrowRepository
              .findBorrowbookAllByState(BorrowState.RETURNED);

      //then
      assertEquals(borrowBookList1.size(), 2);
      assertEquals(borrowBookList2.size(), 2);
      assertEquals(borrowBookList3.size(), 2);
   }
   
   ...

   @BeforeEach // 테스트 메서드를 실행하기 전 수행
   public void setUp(){
      List<Book> bookList = bookList();
      bookRepository.saveAll(bookList);
      borrowRepository.saveAll(borrowList(bookList));
   }

   @AfterEach // 테스트 메서드가 수행하고 난 다음 수행
   public void tearDown(){
      borrowRepository.deleteAll();
      bookRepository.deleteAll();
   }

   // 책 데이터
   public List<Book> bookList(){
      return List.of(
              Book.builder().book_name("name1").build(),
              Book.builder().book_name("name2").build(),
              Book.builder().book_name("name3").build(),
              Book.builder().book_name("name4").build(),
              Book.builder().book_name("name5").build(),
              Book.builder().book_name("name6").build()
      );
   }

   // Borrow 데이터
   public List<Borrow> borrowList(List<Book> books) { 
      return List.of(
              // BORROWING 객체 2개
              Borrow.builder().bookId(books.get(0).getBook_id())
                      .state(BorrowState.BORROWING).createdAt(LocalDate.now())
                      .expiredAt(LocalDate.now().plusDays(7)).borrower(new Borrower(1L, "hong")).build(),
              Borrow.builder().bookId(books.get(1).getBook_id())
                      .state(BorrowState.BORROWING).createdAt(LocalDate.now())
                      .expiredAt(LocalDate.now().plusDays(7)).borrower(new Borrower(1L, "hong")).build(),

              // EXPIRED 객체 2개
              Borrow.builder().bookId(books.get(2).getBook_id())
                      .state(BorrowState.EXPIRED).createdAt(LocalDate.now())
                      .expiredAt(LocalDate.now().plusDays(7)).borrower(new Borrower(1L, "hong")).build(),
              Borrow.builder().bookId(books.get(3).getBook_id())
                      .state(BorrowState.EXPIRED).createdAt(LocalDate.now())
                      .expiredAt(LocalDate.now().plusDays(7)).borrower(new Borrower(2L, "kim")).build(),

              // RETURNED 객체 2개
              Borrow.builder().bookId(books.get(4).getBook_id())
                      .state(BorrowState.RETURNED).createdAt(LocalDate.now())
                      .expiredAt(LocalDate.now().plusDays(7)).borrower(new Borrower(2L, "kim")).build(),
              Borrow.builder().bookId(books.get(5).getBook_id())
                      .state(BorrowState.RETURNED).createdAt(LocalDate.now())
                      .expiredAt(LocalDate.now().plusDays(7)).borrower(new Borrower(2L, "kim")).build()
      );
   }
}

조금 복잡해보이지만 차근 차근 알아보자.

먼저 다른 테스트 함수들에서도 Book 객체들와 Borrow 객체들을 사용하므로 borrowList()bookList()로 공통 메서드로 만들어 두었다.

그리고 테스트를 하기 전 setUp()을 통해 데이터를 넣어넣고, 테스트가 수행하고 나서 tearDown()을 통해 데이터를 삭제하였다. 이는 FIRSTIndependent를 만족할 수 있다.

이제 given - when - then을 살펴보자.

given

setUp 함수를 통해 미리 데이터를 디비에 넣어놨으므로, 수행할게 없다.

when

BORROWING, EXPIRED, RETURNED 상태로 각각 데이터를 가져온다.

then

가져온 데이터들의 갯수들의 수가 맞는지 확인한다.

글을 마치며

테스트를 작성해야한다는 생각은 있었으나 얼마만큼 테스트를 해야하고, 어떻게 시작해야할지 고민이 많았었다.

테스트 커버리지라는 내용을 알고 나서 커버리지를 100% 달성하자라는 목표로 하다보니 테스트가 즐거워진 것 같다.

원래는 라인 커버리지 100%를 목표로 하였으나, 조금 더 나아가 Branch 100%를 달성하는 데 성공하였다.

참조

  • 토스 SLASH 21

  • https://mangkyu.tistory.com/144

  • https://github.com/cheese10yun/spring-guide/blob/master/docs/test-guide.md

comments powered by Disqus