서비스가 레파지토리에 의존적이라 레파지토리까지 IoC에 띄워야할까?
띄워야한다.
하지만 실제 db에 값이 들어가고 안들어가고는 테스트할 필요가 없다.
레파지토리 테스트에서 이미 확인했기 때문이다.
이때 사용하는게 Mock이다. 가짜 데이터를 띄워주는것이다.
레파지토리 테스트할 때 @ExtendWith을 붙여주지 않은 이유는 @DataJpaTest에 포함되어있기 때문이다.
@ExtendWith에 SpringExtension.class를 붙이면 모든게 다 메모리에 떠버리기 때문에
서비스에서는 MockitoExtension.class를 붙여준다.
@ExtendWith(MockitoExtension.class) // 스프링 컨테이너 내부에 있는것처럼 속이기!
public class BookServiceTest {
}
붙이면 내가 지금 스프링 컨테이너 내부에 있다고 속일 수 있어서 IoC에 접근이 가능하다.
서비스의 given은 dto이다.
이걸 컨트롤러를 만들어서 전달해줄게 아니니까 내가 강제로 가짜 데이터(given)를 만들어준다.
@ExtendWith(MockitoExtension.class) // 스프링 컨테이너 내부에 있는것처럼 속이기!
public class BookServiceTest {
@Test
public void 책등록하기_테스트() {
// given
BookSaveReqDto reqDto = new BookSaveReqDto();
reqDto.setTitle("스프링1강");
reqDto.setAuthor("메타코딩");
// when
// then
}
}
given이 끝났으니 save를 테스트할건데 직접 save하진 않을것이다.
레파지토리 영역까지 테스트하진 않을거니까. 분리하자!
하지만 레파지토리까지 테스트해도 상관없다.
다만 느려지고 분리가 되지 않을뿐이지!
가짜로 save하는 방법이 있다.
@Extendwith로 스프링 내부에 들어와있는 척 하는 중이라
BookRepository를 IoC에 띄운적이 없기 때문에 의존성 주입이 불가능하다.
이때 @Mock를 붙여준다.
그러면 실제로 저장되지 않고 가짜로 저장시켜준다.
@ExtendWith(MockitoExtension.class) // 스프링 컨테이너 내부에 있는것처럼 속이기!
public class BookServiceTest {
@Mock
private BookRepository bookRepository;
@Test
public void 책등록하기_테스트() {
// given
BookSaveReqDto reqDto = new BookSaveReqDto();
reqDto.setTitle("스프링1강");
reqDto.setAuthor("메타코딩");
// when
// then
}
}
얘는 가짜로 save한 것이기 때문에 save를 리턴받아보면 null을 돌려준다.
아무동작도 안하고 테스트만 통과할 수 있게 가짜 객체를 만들어준것이다.
스텁 stub 행동정의 = 가설
어차피 bookRepository가 Mockito라서 null을 리턴해주니까
얘가 리턴해줄때 이런값을 리턴해줄거야! 하고 가설을 정의하는것이다.
1. 서비스 테스트는 실제 서비스 메서드를 호출해야한다.
그 메서드 안에서 bookRepository.save( )가 동작할 때 stub이 발동한다.
근데 BookService를 때리려고 하면 오류가 발생한다.
BookService가 BookRepository를 의존하고 있기 때문이다.
BookService를 메모리에 띄워줘야한다.
이때 @InjectMocks 를 사용한다.
@ExtendWith(MockitoExtension.class) // 스프링 컨테이너 내부에 있는것처럼 속이기!
public class BookServiceTest {
@InjectMocks
private BookService bookService;
@Mock
private BookRepository bookRepository;
@Test
public void 책등록하기_테스트() {
// given
BookSaveReqDto reqDto = new BookSaveReqDto();
reqDto.setTitle("스프링1강");
reqDto.setAuthor("메타코딩");
// stub (행동 정의 = 가설)
Mockito.when(bookRepository.save(reqDto.toEntity())).thenReturn(new Book(1L, "스프링1강", "메타코딩"));
// when
BookRespDto respDto = bookService.책등록하기(reqDto); // 실제 메서드 호출하기
// then
assertEquals(1L, respDto.getId());
assertEquals("스프링1강", respDto.getTitle());
assertEquals("메타코딩", respDto.getAuthor());
}
}
2. mockito 영역에 BookRepository가 떠있는지 확인하고
있으면 BookService에 의존성을 주입해준다.
Mockito로 띄운 애들중에 알맞은 것을 찾아 의존성을 주입해주는게 InjectMocks이다.
서비스레이어에서 !!
3. Book이 3번 new 되면 값은 같아도 메모리에 뜬 공간의 주소가 다르다.
그래서 얘들을 내부적으로 비교했을 때 다른 인스턴스로 나온다.
메모리 주소가 다르니까.
그래서 Book에다가 @EqualsAndHashCode를 붙여주면 주소를 비교하지 않고 값만 비교한다.
@EqualsAndHashCode // 주소비교 안함, 값만 비교함
@Getter
@NoArgsConstructor
@Entity
public class Book {
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Id
private Long id;
private String title;
private String author;
@Builder
public Book(Long id, String title, String author) {
this.id = id;
this.title = title;
this.author = author;
}
}
stub 정의할 때 new한 Book이랑
실제 서비스단에서 save할 때 stub이네? 하고 new 해주는 Book을 비교했는데
메모리 주소가 달랐기 때문에 오류가 난것이다.
근데 이렇게 하는거보다 주소까지 굳이 비교하지 말라고 침묵 어노테이션을 걸어주는게 더 나을것같다.
당연히 stub으로 정의한 값이 리턴 되어서 then 결과가 true일 수 밖에 없지 않냐 생각할 수 있지만
서비스로직이 짧아서 그렇지 사실 더 많은 로직이 들어가면 당연한 그 값이 안나올 수도 있다.
[출처]
https://cafe.naver.com/metacoding
메타 코딩 유튜브
https://www.youtube.com/c/%EB%A9%94%ED%83%80%EC%BD%94%EB%94%A9