Search code examples
javaspring-bootjunitmockitospring-test

Stubbing void method of spybean failing


I have a void method bar in a service Foo as shown:

@Component
@EnableAsync
@AllArgsConstructor
public class Foo {
    private final SomeOtherService someOtherService;
    private final KafkaListenerEndpointRegistry registry;
    private final FooConfig fooConfig;

    @Async("myExecutor")
    public void bar(A a, B b, ConcurrentSkipListMap<Long, String> c, int d, int e) {
        someOtherService.doSomething(a, b, c, d, e);
    }
}

I am using Foo in MyService class,

@Service
@AllArgsConstructor
public class MyService {
    private final Foo foo;
        
    public void execute() {
    // call foo.bar();
    }
}

Now, I am trying to write a unit test for a case when calling foo.bar() can throw a RuntimeException. Here is my test class:

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringRunner;

import org.springframework.boot.test.context.ConfigDataApplicationContextInitializer;

import java.util.concurrent.ConcurrentSkipListMap;

@RunWith(SpringRunner.class)
@ContextConfiguration(classes = {SpringBootMain.class},
            initializers = {ConfigDataApplicationContextInitializer.class})
@TestPropertySource(properties = {"spring.config.location=classpath:application-test.yml"})
public class MyServiceTest {

    @MockBean
    private SomeOtherService someOtherService;

    @SpyBean
    @Autowired
    private Foo foo;

    @Test
    public void test() {
        MyService myService = new MyService(foo);
        Mockito.doThrow(new RuntimeException("failed")).when(foo).bar(any(A.class), any(B.class), any(ConcurrentSkipListMap.class), anyInt(), anyInt());
        myService.execute();
        // some assertions
    }
}

But I am getting this error while running the test:

WARN org.springframework.test.context.TestContextManager -- 
Caught exception while invoking 'afterTestMethod' callback on TestExecutionListener [org.springframework.boot.test.mock.mockito.ResetMocksTestExecutionListener] 
for test method [public void MyServiceTest.test() throws com.fasterxml.jackson.core.JsonProcessingException] 
and test instance [MyServiceTest@60fc4450]
org.mockito.exceptions.misusing.UnfinishedStubbingException: Unfinished stubbing detected

E.g. thenReturn() may be missing.
Examples of correct stubbing:
    when(mock.isOk()).thenReturn(true);
    when(mock.isOk()).thenThrow(exception);
    doThrow(exception).when(mock).someVoidMethod();
Hints:
 1. missing thenReturn()
 2. you are trying to stub a final method, which is not supported
 3. you are stubbing the behaviour of another mock inside before 'thenReturn' instruction is completed

org.mockito.exceptions.misusing.InvalidUseOfMatchersException: 
Invalid use of argument matchers!
1 matchers expected, 5 recorded:

This exception may occur if matchers are combined with raw values:
    //incorrect:
    someMethod(any(), "raw String");
When using matchers, all arguments have to be provided by matchers.
For example:
    //correct:
    someMethod(any(), eq("String by matcher"));

For more info see javadoc for Matchers class.

I have tried other configurations as well including:

Mockito.doThrow(new RuntimeException("failed")).when(foo).bar(any(A.class), any(B.class), Mockito.<ConcurrentSkipListMap<Long, String>>any(), anyInt(), anyInt());
Mockito.doAnswer(inv -> {
    throw new RuntimeException("failed");
}).when(foo).bar(Mockito.any(A.class), 
                 Mockito.any(B.class), 
                 Mockito.any(ConcurrentSkipListMap.class), 
                 Mockito.anyInt(), 
                 Mockito.anyInt());

But nothing worked so far. Any help is appreciated.


Solution

  • While I can't figure out the mistake in your code since several classes are involved, I can provide you with a solution with imports that works. It may help you.

    import static org.mockito.ArgumentMatchers.any;
    import static org.mockito.ArgumentMatchers.anyInt;
    
    import java.util.concurrent.ConcurrentSkipListMap;
    
    import org.junit.jupiter.api.Test;
    import org.junit.jupiter.api.extension.ExtendWith;
    import org.mockito.Mockito;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.mock.mockito.MockBean;
    import org.springframework.boot.test.mock.mockito.SpyBean;
    import org.springframework.test.context.TestPropertySource;
    import org.springframework.test.context.junit.jupiter.SpringExtension;
    
    import so.A;
    import so.B;
    import so.Foo;
    import so.MyService;
    import so.SomeOtherService;
    
    @ExtendWith(SpringExtension.class)
    @TestPropertySource(properties = { "spring.config.location=classpath:application-test.yml" })
    public class MyServiceTest {
    
        @MockBean
        private SomeOtherService someOtherService;
    
        @SpyBean
        @Autowired
        private Foo foo;
    
        @Test
        public void test() {
        MyService myService = new MyService(foo);
    
        Mockito.doThrow(new RuntimeException("failed")).when(foo).bar(any(A.class), any(B.class),
            any(ConcurrentSkipListMap.class), anyInt(), anyInt());
    
        myService.execute();
        // some assertions
        }
    }
    

    Here are the dummy classes that I created that was not part of your question:

    public class A {}
    
    public class B {}
    
    public class SomeOtherService {
    
        public void doSomething(A a, B b, ConcurrentSkipListMap<Long, String> c, int d, int e) {
        }
    }