해당 포스팅에선 JUnit 에서 트랜잭션이 어떤 식으로 동작하는지 알아 볼것이다!
메서드 실행(트랜잭션 시작) -> 종료(트랜잭션 종료) -> RollBack
실제 서버에서는 RunTimeException이 발생할 때 RollBack이 되고, 이슈가 없다면 Commit이 된다.
BeforeEach 메소드를 실행한 다음에 @Sql("classpath:db/tableInit.sql")을 실행하면 테이블이 삭제되는 거니까 BeforeEach 메소드에서 저장된 데이터는 없어야 한다. 하지만 BeforeEach에서 저장된 데이터가 그대로 남게 됨.
🤷♀️ 왜 테이블을 삭제했는데 데이터가 그대로 남아있지❓
트랜잭션이 시작되고, save된 데이터는 HDD(하드디스크)에 저장되는 것이 아니라, 메모리(램)에 저장이 된다. 이때 @Sql("classpath:db/tableInit.sql")을 통해서 테이블을 삭제하고 초기화 하는 작업은 HDD에 저장된 데이터를 삭제해주는 것이지, 메모리(램)에 저장된 데이터를 삭제하는 것이 아니다.
Commit : 메모리(램)에 존재하는 data를 HDD(하드디스크)에 저장하는 것
RollBack : 메모리(램)에 존재하는 data를 HDD에 저장하지 않고, 메모리의 데이터를 삭제하는 것
h2처럼 임의로 만든 db가 아닌 실제 서버에서 검증을 해야 한다면 id 검증 말고 다른 방식으로 검증 해주자.
// 2. 책목록보기
public List<BookRespDto> 책목록보기(){
return bookRepository.findAll().stream()
.map((new BookRespDto() :: toDto)
.collect(Collectors.toList));
// map으로 해주는 이유 : map으로 하면 stream으로 들어온 object를 복제하여 다른 타입으로 변경이 가능하다.
}
Mockito란?
· Mock 객체를 쉽게 만들고, 관리하고, 검증할 수 있는 방법을 제공하는 프레임워크
애플리케이션에서 데이터베이스, 외부 API 등을 테스트할 때, 해당 제품들이 어떻게 작동하는지 항상 사용하면서 테스트를 작성한다면 매우 불편할 것이다. 이럴 때 어떻게 작동하는지 예측을 하여 Mock 객체를 만들어서 사용하면, 편리한 테스트가 가능하다.
package site.metacoding.junitproject.service;
import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.beans.factory.annotation.Autowired;
import site.metacoding.junitproject.domain.BookRepository;
import site.metacoding.junitproject.util.MailSender;
import site.metacoding.junitproject.web.dto.BookRespDto;
import site.metacoding.junitproject.web.dto.BookSaveReqDto;
@ExtendWith(MockitoExtension.class)
public class BookServiceTest {
@InjectMocks
private BookService bookService; // 얘는 autowired로 메모리에 못 올린다.
@Mock
private MailSender mailSender;
@Mock
private BookRepository bookRepository;
@Test
public void 책등록하기_테스트(){
// given (파라미터로 들어올 데이터)
BookSaveReqDto dto = new BookSaveReqDto();
dto.setTitle("junit강의");
dto.setAuthor("이보통");
// stub (가설)
when(bookRepository.save(any())).thenReturn(dto.toEntity()); // 행동정의 - return 값을 정하는 것(thenReturn값으로 리턴값을 대체해준다.)
when(mailSender.send()).thenReturn(true); // 가설이라고 생각하면 됨
// when (실행)
BookRespDto bookRespDto = bookService.책등록하기(dto);
// then (검증)
assertThat(dto.getTitle()).isEqualTo(bookRespDto.getTitle()); // assertThat : 실제값, isEqualTo : 내가 기대하는 값
assertThat(dto.getAuthor()).isEqualTo(bookRespDto.getAuthor());
}
@Test
public void 책목록보기_테스트(){
// given (파라미터로 들어올 데이터)
// stub (가설)
List<Book> books = new ArrayList<>();
books.add(new Book(1L, "junit강의", "메타코딩"));
books.add(new Book(2L, "spring강의", "겟인데어"));
when(bookRepository.findAll()).thenReturn(books); // books가 return되도록 설정
// when (실행)
List<BookRespDto> dtos = bookService.책목록보기();
// then (검증)
assertThat(dtos.get(0).getTitle()).isEqualTo("junit강의");
}
}
🔧 Repository에 어떤값을 넣든 가짜 클래스이기 때문에 제대로 된 값이 나올 수가 없다. 따라서 when().thenReturn()을 통해서 내가 원하는 값이 return 되도록 설정을 해준다.
⭐️즉, 행동정의 를 해주는 것이다.⭐️
assertEquals보다는 assertThat이 더 좋은 이유
실패하였다,,
실패원인 : "junit강의"를 기대했는데 "spring강의"를 return하였다.
본코드에 문제가 있는 것으로 예상됨.
// 2. 책목록보기
public List<BookRespDto> 책목록보기(){
List<BookRespDto> dtos = bookRepository.findAll().stream()
.map((bookPS) -> new BookRespDto().toDto(bookPS))
.collect(Collectors.toList()); // map으로 해주는 이유 : map으로 하면 stream으로 들어온 object를 복제하여 다른 타입으로 변경이 가능하다.
return dtos;
}
위와 같이 수정해주고 테스트 코드를 다시 실행해주면 테스트 성공한다