본문 바로가기

트러블슈팅

Mockito에서 의존성 주입 받은 Spy 객체를 다른 Spy객체에 의존성 주입하기

문제

다이어그램

InternalDocumentService라는 객체의 테스트 코드를 작성해야했다.

InternalDocumentService 객체의 의존성은 대략 아래 다이어그램과 같다.

InternalDocumentService의 다른 메소드에서도 Document Repository와 PdfGenerator를 사용했다.

 

InternalDocumentService는 BillingStatementCreator, BillingStatementDetailCreator, DocumentRepository, PdfGenerator가 필요했다.

BillingStatementCreator와 BillingStatementDetailCreator는  DocumentRepository, PdfGenerator가 필요했다.

테스트 코드

Mockito 테스트 코드 작성 시, 아래와 같이 작성했다.

 

다음과 같은 목적으로 위 테스트 코드를 작성했다.

 

1) PdfGenerator와 DocumentRepository는 Mock 객체로 만든다.

 

2) @InjectMocks가 붙은 BillingStatementCreator와 BillingStatementDetailCreator에 Mock으로 생성된 PdfGenerator와 DocumentRepository 객체가 주입된다. BillingStatementCreator와 BillingStatementDetailCreator는 Spy객체로 생성된다.

 

3) Mock으로 생성된 PdfGenerator와 DocumentRepository 객체, Spy로 생성된 BillingStatementCreator와 BillingStatementDetailCreator 객체가 모두 @InejctMocks가 붙은 InternalDocumentService에 주입된다. InternalDocumentService는 Spy 객체로 생성된다.

테스트 실행 결과

InternalDocumentService 실행되는 메소드에서 BillingStatementCreator가 null이라고 NullPointException 에러가 났다.

배경 설명

Mockito란?

Mock 객체를 쉽게 만들고, 관리하고, 검증할 수 있는 방법을 제공하는 프레임워크이다.

 

스프링과 Junit을 이용해서 테스트 코드를 작성하다 보면 테스트 환경(database, api)을 구현하는 코드까지 작성해야 하고 실제 테스트할 코드보다 환경을 구현하는 코드가 훨씬 더 복잡해지게 된다. 이런 문제 영역을 해결하기 위해서 Test Double 이라는 것이 나왔고 Java진영에서는 대표적으로 Mockito가 있다.

 

Test Double은 테스트를 목적으로 프로덕션 오브젝트를 대체하는 오브젝트를 뜻한다. 영어권에서는 스턴트 맨을 스턴트 더블이라고 한다. 그러니까 테스트 더블은 말하자면 테스트를 목적으로 진짜 오브젝트를 대신하는 테스트 계 스턴트맨이라고 볼 수 있다.

 

@Mock

Mock 객체로 만들어주는 어노테이션이다. 어노테이션 대신, Mockito.mock()을 이용해서 Mock 객체를 만들 수 있다.

Mock 객체는 테스트 코드에서 행위 조작이 가능한 형태만 같은 껍데기이다.

Mock 객체에 대한 기대 행위를 작성하여 테스트에서 원하는 상황을 설정할 수 있다. 이런 기대 행위를 작성하는것을 Stub이라 한다.

 

아래와 같이 when, thenReturn으로 테스트 코드에서 Mock 객체에 대한 기대 행위를 작성할 수 있다.

 

@Spy

Spy 객체로 만들어주는 어노테이션이다. 어노테이션 대신, Mockito.spy()를 이용해서 Spy 객체를 만들 수 있다.

Spy 객체는 껍데기만 있는 Mock 객체와 다르게 실제로 구현된 기능이 돌아가는 객체이다. 

Spy 객체의 일부 기능을 Mock 객체처럼 Stub할 수 있다.

 

@InjectMocks

@InjectMock은 DI를 @Mock이나 @Spy로 생성된 mock 객체를 자동으로 주입해주는 어노테이션이다.

 

이 외, @MockBean, @SpyBean 등 Mockito에 대한 자세한 내용은 아래 블로그 글을 참고

https://cobbybb.tistory.com/16

 

Mockito @Mock @MockBean @Spy @SpyBean 차이점

예제 코드 https://github.com/cobiyu/MockitoSample Test Double이 왜 필요한 지부터 시작하는 기본적인 테스트 코드부터 한 단계씩 발전시켜나가며 Mockito의 어노테이션들의 정확한 쓰임새에 대해 살펴보겠습

cobbybb.tistory.com

문제 원인

문제 원인은 @InjectMocks로 의존성 주입을 받아 생성된 BillingStatementCreator와 BillingStatementDetailCreator 객체가 알아서 다시 InternalDocumentService에 의존성이 주입되지 않는다는 것이다.

 

BillingStatementCreator와 BillingStatementDetailCreator에 @Spy도 붙였기 때문에, 알아서 @InjectedMocks가 붙은 InternalDocumentService에 의존성이 주입될 줄 알았으나, 그러지 않았다.

 

의존성 주입이 되지 않아 생긴 문제이기 때문에, 직접 의존성 주입을 해주기로 했다.

문제 해결

아래와 같이 테스트 코드를 수정하였다.

    @Mock
    private PdfGenerator pdfGenerator;
    @Mock
    private DocumentRepository documentRepository;
    
    private BillingStatementCreator billingStatementCreator;

    private BillingStatementDetailCreator billingStatementDetailCreator;

    private InternalDocumentService documentService;

    @Before
    public void setup() throws IOException {
        MockitoAnnotations.initMocks(this);
        billingStatementCreator = new BillingStatementCreator(documentRepository, pdfGenerator);
        billingStatementDetailCreator = new BillingStatementDetailCreator(documentRepository, pdfGenerator);

        billingStatementCreator = Mockito.spy(billingStatementCreator);
        billingStatementDetailCreator = Mockito.spy(billingStatementDetailCreator);

        documentService = new InternalDocumentService(pdfGenerator, documentRepository, billingStatementDetailCreator, billingStatementCreator);
        documentService = Mockito.spy(documentService);
    }

 

@Spy와 @InjectMocks 어노테이션이 해준 일을, 직접 setup()메서드에서 코드로 작성해주었다.

 

1) 의존성 주입 받을 BillingStatementCreator, BillingStatementDetail, InternalDocumentService 객체에서 @Spy와 @InjectMocks 어노테이션을 제거해줬다.

2) setup에서 직접 생성자로 BillingStatementCreator, BillingStatementDetail, InternalDocumentService 객체를 생성해준다. @InjectMocks 어노테이션이 하던 일을 대체해줬다.

3) Spy객체로 만들기 위해, Mockito.spy() 메서드를 이용해 BillingStatementCreator, BillingStatementDetail, InternalDocumentService 모두 스파이 객체로 만들어줬다.

 

테스트가 통과되었다.

참고 자료

https://cobbybb.tistory.com/16

 

Mockito @Mock @MockBean @Spy @SpyBean 차이점

예제 코드 https://github.com/cobiyu/MockitoSample Test Double이 왜 필요한 지부터 시작하는 기본적인 테스트 코드부터 한 단계씩 발전시켜나가며 Mockito의 어노테이션들의 정확한 쓰임새에 대해 살펴보겠습

cobbybb.tistory.com

 

https://stackoverflow.com/questions/6300439/multiple-levels-of-mock-and-injectmocks

 

Multiple levels of @Mock and @InjectMocks

So I understand that in Mockito @InjectMocks will inject anything that it can with the annotation of @Mock, but how to handle this scenario? @Mock private MockObject1 mockObject1; @Mock private

stackoverflow.com

https://stackoverflow.com/questions/45514907/why-cannot-we-create-spy-for-parameterized-constructor-using-mockito

 

why cannot we create spy for Parameterized Constructor using Mockito

I have only parameterized constructor in my code and i need to inject through it. I want to spy parameterized constructor to inject mock object as dependency for my junit. public RegDao(){ //ori...

stackoverflow.com

https://javadoc.io/static/org.mockito/mockito-core/3.5.10/org/mockito/Mockito.html#RETURNS_DEFAULTS

 

Mockito (Mockito 3.5.10 API)

Use doCallRealMethod() when you want to call the real implementation of a method. As usual you are going to read the partial mock warning: Object oriented programming is more less tackling complexity by dividing the complexity into separate, specific, SRPy

javadoc.io

https://github.com/mockito/mockito/issues/2459

 

when I use @InjectMocks with @Spy Annotation together I get a question · Issue #2459 · mockito/mockito

Thanks for you provide mocktio plugin First I want to use mockito 4.0 to test full link code in my business scene so I find a strange situation when I initialize this testing instance using @Inject...

github.com