난관
서비스 계층에 대한 슬라이스 테스트를 작성하던 중, 저는 큰 난관에 봉착했습니다.
바로 void
를 반환하는 메서드를 테스트해야하는 상황을 마주한 것이었는데요, 먼저 상황을 같이 볼까요?
@Transactional
@Service
public class ProductService {
private final ProductDao productDao;
public ProductService(final ProductDao productDao) {
this.productDao = productDao;
}
public void addProduct(final CreateProductRequest request) {
final Product product = new Product(request.getName(), request.getImageUrl(), request.getPrice());
productDao.save(ProductEntity.from(product));
}
...
}
위 코드에서 제가 테스트해야 할 메서드는 addProduct()
였습니다.
이 메서드는 요청을 통해 만들어진 상품을 DB 계층에 추가하는 일을 하고 있죠.
여기서 제가 마주한 문제는 다음 2가지였습니다.
addProduct()
메서드의 반환형이void
라는 점- 통합 테스트 환경이 아닌 슬라이스 테스트 환경이라는 점
슬라이스 테스트 환경이다보니 ProductDao
에는 Mock 객체가 주입된 상태였는데, addProduct()
메서드의 반환값이 void
이다보니 상태를 검증할 수도 없어서 대체 무엇을 검증해야할지 감이 안잡혔습니다.
결과적으로는 아래와 같은 테스트 코드를 작성하게 되었습니다.
@ExtendWith(MockitoExtension.class)
public class ProductServiceTest {
@InjectMocks
private ProductService productService;
@Mock
private ProductDao productDao;
@Test
void 상품을_추가한다() {
final CreateProductRequest request = new CreateProductRequest("name", "imageUrl", 1000);
given(productDao.save(any())).willReturn(null);
assertThatCode(() -> productService.addProduct(request))
.doesNotThrowAnyException();
}
...
}
제가 작성하긴 했지만, 꽤나 마음에 들지 않았습니다.
첫 번째는 위 테스트를 통해 무엇을 검증하고자 하는지 전혀 나타나지 않아보였고, 두 번째는 단순 커버리지 올리기용 테스트인 듯 했습니다.
그래서 다시 고민해봤습니다.
addProduct()
테스트는 무엇을 검증해야하나?
행위를 검증하자
다시 생각해보니, 꽤나 명료한 답이 존재했습니다.
void
를 반환하기 때문에 상태를 검증하기 어렵다면, 행위를 검증하면 되지 않을까요?
addProduct()
메서드의 책임은 어디까지일까요?
먼저, “DB 계층에 상품 정보를 저장하는 책임”은 ProductDao
의 책임입니다. ProductService
의 책임이 아니죠.
ProductService의 책임은 그저 ProductDao의 여러 메서드 중에서 가장 적절한 메서드를 호출할 책임만이 있을 뿐입니다.
그중에서도 addProduct()
메서드는, ProductDao
의 여러 메서드 중에서 “상품 저장을 하기에 가장 적절한 메서드인” productDao.save()
를 호출할 책임이 있습니다.
그렇다면 이제 addProduct()
테스트가 검증해야할 내용은 정해졌습니다.
CreateProductRequest
에 대응되는 적절한 ProductEntity
가 있을 때, productService.addProduct()
메서드는 productDao.save(productEntity)
메서드를 호출하는가?
어떻게 행위를 검증할까?
어떤 행위를 검증할지 정했으니, 이제 그 행위를 어떻게 검증할지에 대해 고민해봐야 합니다.
다행히도 행위를 검증하는 대표적인 방법이 존재하는데, 바로 Mock 객체를 사용하는 것입니다.
혹시나 Mock 객체에 대해 잘 모르신다면 아래의 글들을 추천합니다.
저는 Mockito 라이브러리를 사용해서 행위를 검증하였는데, 그 코드는 아래와 같습니다.
@ExtendWith(MockitoExtension.class)
public class ProductServiceTest {
@InjectMocks
private ProductService productService;
@Mock
private ProductDao productDao;
@Test
void 상품을_추가한다() {
final CreateProductRequest request = new CreateProductRequest("name", "imageUrl", 1000);
final ProductEntity productEntity = new ProductEntity("name", "imageUrl", 1000);
productService.addProduct(request);
verify(productDao).save(productEntity); // 행위 검증
}
...
}
먼저 Mockito 라이브러리의 verify()
라는 메서드를 통해 행위 검증을 할 수 있습니다.
위와 같이 productService.addProduct(request)
를 실행시켰을 때, productDao.save(productEntity)
메서드가 실행되었는지를 verify()
메서드를 통해 검증하는 식입니다.
결론
어떤 테스트가 좋은 테스트일까요?
제가 생각했을 때의 테스트의 목적 중 하나는, 실제로 동작하는 문서화 작업이라고 생각합니다.
같은 테스트 결과를 낸다고 한들, 무엇을 검증하고자 하는지 잘 나타내지 못한다면 이는 좋은 문서화가 아닙니다.
이런 관점에서 보았을 때, 처음 작성된 테스트 코드보다는 Mockito를 활용한 행위 검증 테스트 코드가 더 낫다고 생각합니다. 적어도 무엇을 검증하고자 하는지가 더 명확하게 표현되고 있기 때문입니다.
'개발 > Java' 카테고리의 다른 글
Repository 계층, 도메인과 영속성 엔티티 사이의 간극 메꾸기 (0) | 2023.05.21 |
---|---|
public VS private + getter (0) | 2023.04.02 |
취약한 기반 클래스 문제 (2) | 2023.03.26 |
JDBC, DB 접근을 위한 자바 표준 인터페이스 (0) | 2023.03.12 |
[Java] 업 캐스팅, 그리고 OCP 원칙 (2) | 2022.10.04 |
댓글