로직과의 사투/Test

Mockito를 이용한 단위테스트 겉핥기

1. 단위 테스트란?

단위 테스트는 컴퓨터 프로그래밍에서 소스 코드의 특정 모듈이 의도된 대로 작동하는 지 검증하는 절차이다. 즉 모든 함수와 메소드에 대한 테스트 케이스를 작성하는 절차.

- 위키피디아 단위 테스트

잘 갖춰진 단위 테스트 코드들이 존재한다면, 리팩토링 또는 레거시 코드 수정에 자신감을 가질 수 있게 된다. 코드 수정을 통해 변경된 이후의 코드들이 정상 동작하는 지 단위 테스트를 통해 파악 가능해지기 때문이다.

단위 테스트는 테스트 대상 코드를 정확하게 파악하는 것이 중요하다. 비즈니스 로직이 들어간 레이어의 코드가 단순하다면 크게 문제되지 않지만, 일반적으로 아래와 같은 상황들이 단위 테스트 대상 코드를 식별하기 어렵게 만든다.

  • 외부 API와의 통신
  • DB CRUD
  • 불필요한 데이터 생성
  • 잘못된 모듈화로 인해 결합도가 높은 코드

위와 같은 상황들은 단위 테스트를 수행하고자 함에 있어 일반적으로 테스트 코드 수행시간이 길어지게 만들고, 테스트 대상 범위가 넓어져 실제 테스트 대상에 집중할 수 없게 만든다. 이와 같은 상황을 타개하고자 Mock객체를 이용해 단위 테스트를 진행한다.

2. Mock

Mock이란 껍데기 객체를 뜻한다. 진짜 객체와 비슷하게 동작하지만, 개발자가 동작을 컨트롤 (Stubbing) 할 수 있다. Java의 경우 Mockito라이브러리를 통해 손쉽게 Mocking을 수행할 수 있다.

3. Mockito를 활용해 Mocking, Stubbing 하기

아래의 IssuingService.java와 같이 가맹점 정보를 DB로부터 가져와 정상 가맹점이라면 전표를 발행 시키는 서비스 레이어의 코드가 있다고 가정하자.

@Service
@RequiredArgsConstructor
public class IssuingService {


    private final ShopRepository;

    private final DocRepository;
    
    public IssuingResponse issue(IssuingRequest issuingRequest) throws Exception {
    	ShopDto shopDto = shopRepository.findById(issuingRequest.getShopId());
        
        validShopStatus(shopDto);
        
        String docId = docRepository.saveDoc();
        IssuingResponse issuingResponse = new IssuingResponse();
        issuingResponse.setDocId(docId);
        issuingResponse.setShopDto(shopDto);
        
        return issuingResponse;
    }
    
    private void validShopStatus(ShopDto shopDto) {
    	if (ShopStatus.ABNORMAL.getValue().equals(shopDto.getShopStatus())) {
        	throw new ServiceException("500");
        }
    }

}

위 코드에서 실제 테스트 대상이 될 코드는 validShopStatus 메소드이다. Mocking없이 해당 메소드를 테스트하기 위해선

  1. DB와 커넥션
  2. ShopDto 테스트 데이터 인서트
  3. ShopDto 테스트 데이터 셀렉트
  4. 테스트

의 다소 번잡한 과정을 거쳐야 한다. DB와 실제 CRUD를 시행하기 위해 많은 리소스를 낭비하게 되는 것이다. 특히 실제 테스트 대상 코드와 무관한 소스를 작성해 테스트를 시행하기 때문에 실제 테스트 대상 코드의 존재가 희미해진다. 이때 필요한 것이 Mockito 라이브러리를 활용한 Mocking이다.

@ExtendWith(MockitoExtension.class)
public class IssuingServiceTest {

    @Mock
    ShopRepository shopRepository;
    
    @Mock
    DocRepository docRepository;
    
    @InjectMocks
    IssuingService issuingService;
    
    @Test
    @DisplayName("비정상 매장일 때 Exception 체크")
    void test_issuingFailureWithAbnormalShop() throws Exception {
        // given
        IssuingRequest issuingRequest = new IssuingRequest();
        issuingRequest.setShopId("0001");
        
        ShopDto shopDto = new ShopDto();
        shopDto.setShopId("0001");
        shopDto.setShopStatus(ShopStatus.ABNORMAL.value());
        
        // Stubbing을 명시해준다.
        when(shopRepository.findById(issuingRequest.getShopId()))
            .thenReturn(shopDto);
            
        // when
        ServiceExcetpion serviceException = assertThrows(ServiceException.class, () -> {
            issuingService.issue(issuingRequest);
        });
        
        // then
        assertEquals("500", serviceException.getCode());
    }

}

@ExtendWith(MockitoExtension.class)
-> 해당 테스트 클래스가 Mockito 라이브러리를 활용한다는 것을 알린다.

@Mock
-> 실제 구현된 객체 대신 Mock으로 사용하게 될 클래스로 명시한다.

@InjectMocks
-> Mock객체를 사용하게 될 클래스를 의미한다. 테스트 런타임 시 클래스 내부에 선언된 멤버 변수들 중 Mock으로 등록된 클래스에 실제 객체 대신 Mock객체가 주입된다. 

when(shopRepository.findById(issuingRequest.getShopId())).thenReturn(shopDto);
-> shopRepository.findById 메소드가 호출 될 때 응답 값을 테스트 코드에 명시된 shopDto로 Stubbing.

4. 결론

Mockito를 활용한 테스트 더블(Stub, Mock) 사용 테스트 방법에 대해 간단하게 확인했다. 참고할 점은 테스트 더블을 이용하는 것이 반드시 옳은 단위 테스트로 이어지는 것은 아니라는 것이다. 테스트 더블이 없는 통합 테스트도 반드시 필요하다. Mock 객체를 활용한 단위 테스트는 단위 테스트 수준에서 테스트가 통과되는 것이 각 모듈, 레이어 간 연동이 정상이라는 것을 뜻하진 않는다. Mocking에 매몰된 단위 테스트를 작성할 것이 아닌, 상황에 맞게 올바른 테스트 코드를 작성하는 것에 많은 연습을 기해야할 것이다.

참고

테스트가 해당 메소드를 실제로 실행시켰는 지 확인하는 방법은 1. 디버깅 모드 실행, 2. 코드 커버리지(Intellij 등) 등으로 확인 가능하다.