하나의 함수에는 하나의 기능만 가지고 있는 것이 좋다.
하나의 함수에 여러 가지 기능을 섞어 놓는다면 문제가 생겼을 시에 어디에서 문제가 발생했는지 알 수가 없다.
만약 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 메소드의 트랜잭션 또한 종료 된다. 그래서 저장되어 있는 책들을 불러오면 사이즈가 1이 나온다.
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")를 붙여주는 것이 좋다.
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개의 결과가 제대로 나오는 것을 볼 수 있다.
🧐학습 초기라면 눈으로 계속 확인해보는 것도 중요한 과정 중 하나!!