[JUnit]테스트 코드 배우기(3)

gamja·2023년 1월 15일
0
post-thumbnail

⚙️기능 테스트 시 확인해야 할 것들

  • Controller : 클라이언트와 테스트
  • Service : 기능들이 트랜잭션을 잘 타는지
  • Repository : DB쪽 관련 테스트

⚙️기능 구현 시 주의해야 할 점

하나의 함수에는 하나의 기능만 가지고 있는 것이 좋다.
하나의 함수에 여러 가지 기능을 섞어 놓는다면 문제가 생겼을 시에 어디에서 문제가 발생했는지 알 수가 없다.

만약 1,2,3,4 단계를 거쳐서 사과 주스를 만드는 프로그램이 있을 때, 2번 메소드의 코드를 수정했을 때 4번 메소드에서 오류가 발생할 수 있다. 이런 경우 테스트 코드를 통하여 미리 2번 메소드의 코드를 수정해본 뒤, 4번이 연쇄적으로 오류가 난다면 4번 코드를 함께 수정해주면 된다.

  • 하나의 메소드에는 하나의 기능 작성
  • 각 기능마다 테스트 코드를 작성해주면 유지 보수하기 좋다.

🧐단일 테스트뿐만 아니라 통합 테스트도 필요하다!

부분 코드 하나를 수정함으로써 1~100까지 전체 테스트를 진행한다면 너무 많은 시간이 소요된다.


테스트 코드 작성해보기

package site.metacoding.junitproject.domain;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;

import java.util.List;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.context.jdbc.Sql;


@DataJpaTest    // DB와 관련된 컴포넌트만 메모리에 로딩
public class BookRepositoryTest {
    
    @Autowired
    private BookRepository bookRepository;

    // 테스트를 할 때마다 데이터를 일일이 다 넣기가 번거롭기 때문에 메소드를 만들어준다.
    // @BeforeAll          // 테스트 시작 전에 한 번만 실행
    @BeforeEach       // 각 테스트 시작 전에 한 번씩 실행 - 현재는 모든 메서드 실행 전에 실행되어야 하기 때문에 BeforeEach 사용
    public void 데이터준비(){
         // given (데이터 준비)
         String title = "junit";
         String author = "겟인데어";
         Book book = Book.builder()
             .title(title)
             .author(author)
             .build();
         bookRepository.save(book);
    }
    // 트랜잭션이 어느 시점에서 종료 될까?
    // 가정 1 : [ 데이터준비() + 1 책등록 ] (T) , [ 데이터준비() + 2 책목록보기 ] + (T) -> 사이즈 : 1   (검증 완료) 
    // 가정 2 : [ 데이터준비() + 1 책등록 + 데이터준비() + 2 책목록보기 ] (T) -> 사이즈 2   (검증 실패)

    // 1. 책 등록
    @Test
    public void 책등록_test(){
        // given (데이터 준비)
        String title = "junit";
        String author = "이보통";
        Book book = Book.builder()
        .title(title)
        .author(author)
        .build();

        // when (테스트 실행)
        Book bookPS = bookRepository.save(book);    // bookPS는 영속화가 된 객체, book은 클라이언트로부터 받은 객체

        // then (검증)
        assertEquals(title, bookPS.getTitle());
        assertEquals(author, bookPS.getAuthor());
        // 트랜잭션 종료 (저장된 데이터를 초기화함)

    }

    // 2. 책 목록보기
    @Test
    public void 책목록보기_test(){
        // given (데이터 준비)
        String title = "junit";
        String author = "겟인데어";

        // when
        List<Book> booksPS = bookRepository.findAll();

        System.out.println("사이즈 ===================================== : " + booksPS.size()); // 트랜잭션이 어떻게 진행되는지 확인
        
        //then
        assertEquals(title, booksPS.get(0).getTitle());
        assertEquals(author, booksPS.get(0).getAuthor());
    }   // 트랜잭션 종료 (저장된 데이터를 초기화함)


    // 3. 책 한건 보기
    @Sql("classpath:db/tableInit.sql")
    @Test
    public void 책한건보기_test(){
        // given
        String title = "junit";
        String author = "겟인데어";

        // when
        Book bookPS = bookRepository.findById(1L).get();

         //then
         assertEquals(title, bookPS.getTitle());
         assertEquals(author, bookPS.getAuthor());
    }   // 트랜잭션 종료 (저장된 데이터를 초기화함)

}

@BeforeEach

단위 테스트 시 하나의 메소드가 실행 후 종료되면 트랜잭션이 종료 되면서 저장된 데이터를 초기화한다. 하지만 데이터 수정, 삭제, 조회 등의 기능을 수행하기 위해서는 데이터가 저장되어 있어야 하기 때문에 모든 메소드가 실행되기 전에 초기 작업으로 데이터를 저장해두는 것이다.

@BeforeEach 는 어느 시점에서 트랜잭션이 종료 될까?

하나의 메소드가 실행되기 전에 @BeforeEach 메소드가 실행이 되고, 해당 메소드가 종료되면 @BeforeEach 메소드의 트랜잭션 또한 종료 된다. 그래서 저장되어 있는 책들을 불러오면 사이즈가 1이 나온다.

JUnit 테스트 특징 정리

  1. 메서드 실행 순서가 보장되지 않음 - Order() 어노테이션 사용
  2. 테스트 메서드가 하나 실행 후 종료되면 데이터가 초기화 - @Transactional이 초기화시켜줌 (단, primary key auto_increment 값은 초기화가 안 된다. id=1,2를 삭제했다면 다음에 insert할 때 3부터 추가 됨.)

2번과 같은 특징으로 인해 테스트 시 문제가 발생할 수 있다.
예를 들어 책을 삭제하기 위해서 아래와 같은 간단한 테스트 코드를 작성했다고 하자.


 @BeforeEach       // 각 테스트 시작 전에 한 번씩 실행 - 현재는 모든 메서드 실행 전에 실행되어야 하기 때문에 BeforeEach 사용
 public void 데이터준비(){
         // given (데이터 준비)
         String title = "junit";
         String author = "겟인데어";
         Book book = Book.builder()
             .title(title)
             .author(author)
             .build();
         bookRepository.save(book);
 }
 
 @Test
 public void 책삭제_test(){
      // give
      Long id = 1L;

     // when
     bookRepository.deleteById(id);

     // then
     assertFalse(bookRepository.findById(id).isPresent());   
}   

이때 문제가 되는 것은 이전에 삭제 기록이 존재한 상태에서 데이터를 추가한다면 auto_increment 값이 1이 아니라 이전 id + 1이라는 것이다. id값이 무조건 1일 것이라는 확신이 없는 상황! 이럴 때 @Sql("classpath:db/tableInit.sql") 를 사용하여 테이블을 삭제했다가 다시 create 해줌으로써 문제를 해결한다.

특정 Id값을 찾는 모든 테스트 앞에 @Sql("classpath:db/tableInit.sql")를 붙여주는 것이 좋다.

resource/db/tableInit.sql

tableInit.sql 내용은 아래와 같이 채워주면 된다.

drop table if exists Book; 
 create table Book (
       id bigint generated by default as identity,
        author varchar(20) not null,
        title varchar(50) not null,
        primary key (id)
    )

	// 5. 책 수정
    @Test
    public void 책수정_test(){
        // given
        Long id = 1L;
        String title = "junit5";
        String author = "이보통";
        Book book = new Book(id, title, author);

        // when
        Book bookPS = bookRepository.save(book);

        bookRepository.findAll().stream()   // 모든 책을 찾아서 stream으로 변경해서, for문을 돌면서 하나하나 b변수에 넣고, {}를 실행한다.
        .forEach(b -> {
            System.out.println(b.getId()); 
            System.out.println(b.getTitle());
            System.out.println(b.getAuthor());
            System.out.println("===============");
        } ) ;
      }

단일 테스트를 하는 경우 위와 같이 1개의 데이터가 잘 수정되는 것을 볼 수 있다. 하지만 통합 테스트를 할 경우 아래와 같이 출력된다.

위와 다르게 두 개의 데이터가 나오는 것을 볼 수 있다.
id = 1L로 설정해뒀기 때문에 1에 해당하는 아이디가 없으면 자동으로 아이디를 생성하여 새로운 값을 넣어준 것이다.

제대로 테스트를 하기 위해서는 위에서 말한 초기화 작업@Sql("classpath:db/tableInit.sql")을 테스트 할 메소드 위에 작성 해줘야 한다.

그 결과 1개의 결과가 제대로 나오는 것을 볼 수 있다.

🧐학습 초기라면 눈으로 계속 확인해보는 것도 중요한 과정 중 하나!!

profile
눈도 1mm씩 쌓인다.

0개의 댓글

관련 채용 정보