[JUnit] TDD 방법론(2)

이민우·2023년 10월 3일
1

Spring Boot

목록 보기
13/20

지난 시간에 이어 Junit에대해 배워본다!
TDD 방법론1


진행시 테스트를 진행할 환경은 구축이 되있다 가정하고 진행하겠습니다!

👉 테스트 환경 구축 참조

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

기본 MVC구조를 사용한다 하면

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

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

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

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

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

부분 코드 하나를 수정함으로써 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());
    }   // 트랜잭션 종료 (저장된 데이터를 초기화함)

}

코드를 보면 주석으로 Given/When/Then 처리되있는걸 확인 할 수 있다.
자세히 알아보면

  • Given
    • 테스트를 위해 주어진 상태
    • 테스트 대상에게 주어진 조건
    • 테스트가 동작하기 위해 주어진 환경
  • When
    • 테스트 대상에게 가해진 어떠한 상태
    • 테스트 대상에게 주어진 어떠한 조건
    • 테스트 대상의 상태를 변경시키기 위한 환경
  • Then
    • 앞선 과정의 결과

      이와 같은 규칙은 BDD의 행동 스펙이라고 볼수있다. BDD는 애플리케이션이 어떻게 행동해야 하는지에 대한 공통된 이해를 구성하는 방법이다.

@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

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")을 테스트 할 메소드 위에 작성 해줘야 한다.

profile
백엔드 공부중입니다!

0개의 댓글