JUnit5 & Mockito를 이용한 Spring Service Layer 단위테스트
기술스택 : SpringBoot, Gradle, JPA, Junit5
DB : h2
build.gradle
- assertJ의
assertThat
사용위한
testImplementation("org.assertj:assertj-core:3.23.1")
참고)
- JUnit 의 assertThat
- 메소드들을 import 해놓아야함, 안그럼 자동완성 안됨
- 필요한 메소드 중 데이터타입에 맞는 알맞은 항목 찾아야함
- 추가 조건 검증 시 : allOf이라는 메소드로 기존 조건과 묶어줘야 함
assertThat(result, allOf( greaterThan(0), lessThan(10) ));
- AssertJ의 assertThat
- assertThat에서 반환되는 Assert클래스 사용 -> 자동완성 지원
- 인자의 데이터타입에 맞는 Assert 클래스를 반환 -> 필요한 메소드만 분류되어 있음
- Method Chaining 패턴(메소드 리턴 객체를 받고 이 리턴 객체의 메소드를 호출하는 방법을 반복) -> 가독성 좋음
ex) A.method1().method2()…assertThat(result) .isGreaterThan(0) .isLessThan(10);
BookServiceTest.java
단위테스트 작성준비
@ExtendWith(MockitoExtension.class)
public class BookServiceTest {
@InjectMocks
private BookService bookService;
@Mock
private BookRepository bookRepository;
@Mock
private MailSender mailSender;
}
@ExtendWith(MockitoExtension.class)
: Mockito 의 Mock 객체 사용위한 annotationMockito
: 가짜객체 보관환경- @SpringBootTest : application을 띄우기때문에 통합테스트에 용이(시간도 오래걸리고 무겁기때문에 단위테스트하는데는 알맞지 않음)
@InjectMocks
:가짜환경에 있는 의존성
(@Mock 붙은)까지 주입해줌@Mock
: Mock 객체로(가짜객체) -> Mockito(가짜환경)에 뜸
Mockito 사용 이유
Repository 테스트는 이미 완료했기 때문에 서비스-레파지토리-DB 까지 테스트할 필요가 없음(무겁기만함)
->Repository는 메모리에 로드할 필요 없음
=> 가짜객체 Mock으로!
책 등록 단위테스트 메소드
@DisplayName("책 등록")
@Test
public void insertBook_test() {
//given
BookSaveReqDto dto = new BookSaveReqDto();
dto.setTitle("junit5");
dto.setAuthor("김태희");
//stub(가설)
when(bookRepository.save(any())).thenReturn(dto.toEntity());
when(mailSender.send()).thenReturn(true);
//when
BookRespDto bookRespDto = bookService.insertBook(dto);
//then
assertThat(bookRespDto.getTitle()).isEqualTo(dto.getTitle());
assertThat(bookRespDto.getAuthor()).isEqualTo(dto.getAuthor());
}
책 수정 단위테스트 메소드
@DisplayName("책 수정")
@Test
public void updateBook_test() {
//given
Long id = 1L;
BookSaveReqDto dto = new BookSaveReqDto();
dto.setTitle("spring");
dto.setAuthor("태희");
//stub
Book book = new Book(1L, "junit", "김태희");
Optional<Book> bookOP = Optional.of(book);
when(bookRepository.findById(id)).thenReturn(bookOP);
//when
BookRespDto bookRespDto = bookService.updateBook(id, dto);
//then
assertThat(bookRespDto.getTitle()).isEqualTo(dto.getTitle());
assertThat(bookRespDto.getAuthor()).isEqualTo(dto.getAuthor());
}
Dto
- Domain :
Book.java
- Dto :
BookRespDto.java
,BookSaveReqDto.java
생성
Dto 생성한 이유!
jpa:
open-in-view: true
true일 경우 영속성 컨텍스트가 트랜잭션 범위를 넘어선 레이어(Controller)까지 살아있음 = Controller단까지 영속화된 객체(Persistent Context)
를 넘겨주게 되면
lazy loading
이라는 변수 발생!
참고)
Lazy Loading(지연로딩)
: 연관된 엔티티를 실제 사용하는 시점에 데이터베이스에서 조회Eager Loading(즉시로딩)
: 엔티티를 조회할 때 연관된 엔티티도 함께 조회 (처음부터 연관된 모든 엔티티를 조회해서 영속성 컨텍스트에 올려놓는 것)
과정
1) Controller : Dto를 받아서 Service로
2) Service : Dto를 받아서 Object(book)로 바꿔서 Repository로
3) Repository : Object(book) 받아서 CRUD ex) save()
4) Persistent Context check : 없으면 DB(메모리에)에 book 저장 -> 영속화된 Book 객체(bookPS)
생성
5) Service에서 transaction 종료
+ commit(메모리에서 HDD로 보내서 저장) or rollback(안보내고 메모리에서 지움)
- 값 변경은 끝
6) Controller에서 DB세션 종료
- select는 가능
- 근데 만약, bookPS를 Service에서 넘겨줘서 Controller에서 bookPS.getId()
를 해버리면?
=> 클라이언트로 보낼 때 message converter가 bookPS에 있는 getter때문에 연관관계 있는 애들을 이때 DB에서 끌고오니까 lazy loading
==> 결론 : Service에서 Controller로 Dto로 보내주기!
//BookSaveReqDto.java
public Book toEntity() {
return Book.builder()
.title(title)
.author(author)
.build();
}
//BookService.java
Book bookPS = bookRepository.save(dto.toEntity());
(Controller에서 받은 Dto(dto
)를 Entity(dto.toEntity()
)로 바꿔서 Repository로)
//Book.java
public BookRespDto toDto() {
return BookRespDto.builder()
.id(id)
.title(title)
.author(author)
.build();
}
//BookService.java
return bookPS.toDto();
(Repository에서 받은 Entity(bookPS
)를 Dto(bookPS.toDto()
)로 바꿔서 Controller로)
Reference
https://www.youtube.com/playlist?list=PL93mKxaRDidEZfpXoyWZ-2ZLsYrQByDMP
https://jwkim96.tistory.com/168