본문 바로가기
개발/Java

반환형이 void인 메서드, 어떻게 테스트할까? (with 행위 검증)

by kadokok 2023. 5. 7.

난관

서비스 계층에 대한 슬라이스 테스트를 작성하던 중, 저는 큰 난관에 봉착했습니다.

바로 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가지였습니다.

  1. addProduct() 메서드의 반환형이 void라는 점
  2. 통합 테스트 환경이 아닌 슬라이스 테스트 환경이라는 점

슬라이스 테스트 환경이다보니 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 객체에 대해 잘 모르신다면 아래의 글들을 추천합니다.

 

Mock Object란 무엇인가?

다른 누군가로부터 휴대 전화 서비스(CellphoneService) 기능을 제공 받아 이를 사용한 휴대 전화 문자 발신기(CellphoneMmsSender)를 프로그래밍 한다고 생각해 보자.

medium.com

 

Mocks Aren't Stubs

Explaining the difference between Mock Objects and Stubs (together with other forms of Test Double). Also the difference between classical and mockist styles of unit testing.

martinfowler.com

 

[tdd] 상태검증과 행위검증, stub과 mock 차이

SUT(System Under Test) : 주요 객체(primary object) 협력객체(collaborator) : 부차적 객체(secondary objects) 테스트 더블(Test Double) : 테스팅을 목적으로 진짜 객체대신 사용되는 모든 종류의 위장 객체 Dummy, Fake Ob

joont92.github.io

 

저는 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를 활용한 행위 검증 테스트 코드가 더 낫다고 생각합니다. 적어도 무엇을 검증하고자 하는지가 더 명확하게 표현되고 있기 때문입니다.

댓글