Search code examples
springmockingspockspring-retry

Mock injection in Spring using Spock not working


So I am using the Spring=Retry library in my project and trying to run a test case for that. I am loading the service DCS as a bean that returns a new object. Its two dependencies SSService and AttributeService are loaded as beans as well. But these two are mocks. When my test spec runs i can see inside DCS.execute that the mocks are there alright. But the interactions on them like 1 * SSService.read(_ as LComponent,_) >> mockSimpleSettingCommResult is not taking effect resulting in a null value instead of the value i want it to return.

@ContextConfiguration(classes = [SpringRetryConfig])
class DCSISpec extends Specification {

    @Autowired
    DCS  DCS
    @Autowired
    SSService sSService
    @Autowired
    AttributeService attributeService

    def setup() {
//        DCS.SSService = SSService
//        DCS.attributeService = attributeService
    }

    def "execute failure"(){
        setup:
        DataCollectionDataSet mockDataCollectionDataSet = Mock(DataCollectionDataSet)
        LComponent mockLComponent = Mock(LComponent)
        SSCommResult mockSimpleSettingCommResult = Mock(SSCommResult)

        ReflectionTestUtils.setField(DCS, "SSService", SSService)
        ReflectionTestUtils.setField(DCS, "attributeService", attributeService)

        when:
        DCS.execute(mockLComponent, mockDataCollectionDataSet)

        then:
        1 *  mockSimpleSettingCommResult.getDegreeOfSuccess() >> SSCommResult.DegreeOfSuccess.FAILURE
        1 * mockDataCollectionDataSet.getNamespace() >> DCSNamespace.xyz
        1 * mockDataCollectionDataSet.getDataElements() >> ["FOO": "BAR"]
        1 *  SSService.read(_ as LComponent,_) >> mockSimpleSettingCommResult

        3 * DCS.execute(_ as LComponent, _ as DataCollectionDataSet)
    }
    @Configuration
    @EnableRetry
    public static class SpringRetryConfig {
        @Bean
        public SSService SSService() {
            Mockito.mock(SSService)
        }
        @Bean
        public AttributeService attributeService() {
            Mockito.mock(AttributeService)
        }
        @Bean
        public DCS  DCS() {
           return new DCS();
        }
    }
}

This is the exception i get when trying to use plain Mockito ,

java.lang.NullPointerException
    at org.codehaus.groovy.runtime.callsite.PogoMetaClassSite.call(PogoMetaClassSite.java:41)
    at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:47)
    at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:116)
    at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:136)
    at com.lexmark.mps.cma.service.DataCollectionRetryTest.test_retry(DataCollectionRetryTest.groovy:70)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:45)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:42)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
    at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:73)
    at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:82)
    at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:73)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:263)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:224)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:83)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:231)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:60)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:229)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:50)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:222)
    at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
    at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:68)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:300)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:163)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:157)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:69)
    at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:234)
    at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:74)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)

Solution

  • Given my comments to the question above, I think you are better off using bare Mockito here (w/o Spock as you asked in the comments as I'm struggling on how to make a readable Spock specification for this use case):

    EDIT: The completely correct solution is below this one

    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(classes = DCSTest.SpringRetryConfig.class)
    public class DCSTest {
        @Autowired
        private DCS dcs;
    
        @Test
        public void test_retry() {
            //given:
            LComponent component = mock(LComponent.class);
            DataCollectionDataSet dataSet = mock(DataCollectionDataSet.class);
    
            given(dcs.execute(component, dataSet)) //It's BDDMockito class
                    .willThrow(new RuntimeException("1"))
                    .willThrow(new RuntimeException("2"))
                    .willReturn("Foo");
    
            //when:
            String result = dcs.execute(component, dataSet);
    
            //then:
            verify(dcs, times(3)).execute(component, dataSet);
            assertThat(result, equalTo("Foo"));
        }
    
        @Configuration
        @EnableRetry
        static class SpringRetryConfig {
            @Bean
            DCS dcs() {
                return mock(DCS.class);
            }
        }
    }
    
    public class DCS {
        @Retryable(maxAttempts = 3)
        String execute(LComponent component, DataCollectionDataSet dataSet) {
            return "Bar";
        }
    }
    

    CORRECT SOLUTION:

    Ok, as the OP and discovered the above verify(dcs, times(3)).execute(component, dataSet); doesn't work as expected. No matter what number you use in times() the test will always succeeds. This is because @Retryable creates an aspect around the dcs mock. As a result, each call to dcs.execute is intercepted by Spring and Mockito doesn't actually verify the call. To overcome this, we can create our own aspect around the mock and just as a side effect, count how many times the @Retryable method is called. Below is the working code of such a solution:

    @RunWith(SpringJUnit4ClassRunner.class)
    public class DCSTest {
        @Autowired
        private LComponent component;
        @Autowired
        private DataCollectionDataSet dataSet;
        @Autowired
        private DCS dcs;
        @Autowired
        private RetryCount retryCount;
    
        @Test
        public void test_retry() {
            //when:
            String result = dcs.execute(component, dataSet);
    
            //then:
            assertThat(retryCount.value, equalTo(3));
            assertThat(result, equalTo("Foo"));
        }
    
        @Aspect
        public static class RetryCount {
            public int value = 0;
    
            @Before("execution(* DCS.execute(..))")
            public void advice() {
                value++;
            }
        }
    
        @Configuration
        @EnableRetry
        @EnableAspectJAutoProxy
        public static class SpringRetryConfig {
            @Bean
            DCS dcs() {
                DCS dcs = mock(DCS.class);
                given(dcs.execute(component(), dataSet())) //It's BDDMockito class and take note that better to keep this declaration here so that Spring doesn't intercept the call once Retryable aspect is created
                        .willThrow(new RuntimeException("1"))
                        .willThrow(new RuntimeException("2"))
                        .willReturn("Foo");
                return dcs;
            }
    
            @Bean
            RetryCount retryCount() {
                return new RetryCount();
            }
    
            @Bean
            LComponent component() {
                return new LComponent();
            }
    
            @Bean
            DataCollectionDataSet dataSet() {
                return new DataCollectionDataSet();
            }
        }
    }