【问题标题】:Mocked Spring @Service that has @Retryable annotations on methods fails with UnfinishedVerificationException在方法上具有 @Retryable 注释的模拟 Spring @Service 失败并出现 UnfinishedVerificationException
【发布时间】:2016-09-06 05:16:24
【问题描述】:

我正在使用 Spring Boot 1.4.0.RELEASE 和 spring-boot-starter-batch、spring-boot-starter-aop 和 spring-retry

我有一个 Spring Integration 测试,它有一个在运行时模拟的 @Service。我注意到如果@Service 类在其方法中包含任何@Retryable 注释,那么它似乎会干扰Mockito.verify(),我得到UnfinishedVerificationException。我想这一定与spring-aop有关?如果我注释掉@Service 中的所有@Retryable 注释,则再次验证可以正常工作。

我创建了一个 github project 来演示这个问题。

它在sample.batch.MockBatchTestWithRetryVerificationFailures.batchTest() validateMockitoUsage(); 失败

类似的东西:

12:05:36.554 [main] DEBUG org.springframework.test.context.support.AbstractDirtiesContextTestExecutionListener - After test method: context [DefaultTestContext@5ec0a365 testClass = MockBatchTestWithRetryVerificationFailures, testInstance = sample.batch.MockBatchTestWithRetryVerificationFailures@5abca1e0, testMethod = batchTest@MockBatchTestWithRetryVerificationFailures, testException = org.mockito.exceptions.misusing.UnfinishedVerificationException: 
Missing method call for verify(mock) here:
-> at sample.batch.service.MyRetryService$$FastClassBySpringCGLIB$$7573ce2a.invoke(<generated>)

Example of correct verification:
    verify(mock).doSomething()

但是我有另一个类 (sample.batch.MockBatchTestWithNoRetryWorking.batchTest()) 带有一个模拟的 @Service,它没有任何 @Retryable 注释并且验证工作正常。

我做错了什么?

在我的 pom.xml 我有以下内容:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.4.0.RELEASE</version>
</parent>
...
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-batch</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.retry</groupId>
        <artifactId>spring-retry</artifactId>
    </dependency>
...

然后是所有相关的 Java 类

@SpringBootApplication
@EnableBatchProcessing
@Configuration
@EnableRetry
public class SampleBatchApplication {

    @Autowired
    private JobBuilderFactory jobs;

    @Autowired
    private StepBuilderFactory steps;

    @Autowired
    private MyRetryService myRetryService;

    @Autowired
    private MyServiceNoRetry myServiceNoRetry;

    @Bean
    protected Tasklet tasklet() {

        return new Tasklet() {
            @Override
            public RepeatStatus execute(StepContribution contribution,
                    ChunkContext context) {
                myServiceNoRetry.process();
                myRetryService.process();
                return RepeatStatus.FINISHED;
            }
        };

    }

    @Bean
    public Job job() throws Exception {
        return this.jobs.get("job").start(step1()).build();
    }

    @Bean
    protected Step step1() throws Exception {
        return this.steps.get("step1").tasklet(tasklet()).build();
    }


    public static void main(String[] args) throws Exception {
        // System.exit is common for Batch applications since the exit code can be used to
        // drive a workflow
        System.exit(SpringApplication
                .exit(SpringApplication.run(SampleBatchApplication.class, args)));
    }

    @Bean
    ResourcelessTransactionManager transactionManager() {
        return new ResourcelessTransactionManager();
    }

    @Bean
    public JobRepository getJobRepo() throws Exception {
        return new MapJobRepositoryFactoryBean(transactionManager()).getObject();
    }

}

@Service
public class MyRetryService {

    public static final Logger LOG = LoggerFactory.getLogger(MyRetryService.class);

    @Retryable(maxAttempts = 5, include = RuntimeException.class, backoff = @Backoff(delay = 100, multiplier = 2))
    public boolean process() {

        double random = Math.random();

        LOG.info("Running process, random value {}", random);

        if (random > 0.2d) {
            throw new RuntimeException("Random fail time!");
        }

        return true;
    }

}

@Service
public class MyServiceNoRetry {

    public static final Logger LOG = LoggerFactory.getLogger(MyServiceNoRetry.class);

    public boolean process() {

        LOG.info("Running process that doesn't do retry");

        return true;
    }

}

@ActiveProfiles("Test")
@ContextConfiguration(classes = {SampleBatchApplication.class, MockBatchTestWithNoRetryWorking.MockedRetryService.class}, loader = AnnotationConfigContextLoader.class)
@RunWith(SpringRunner.class)
public class MockBatchTestWithNoRetryWorking {

    @Autowired
    MyServiceNoRetry service;

    @Test
    public void batchTest() {
        service.process();

        verify(service).process();
        validateMockitoUsage();
    }

    public static class MockedRetryService {
        @Bean
        @Primary
        public MyServiceNoRetry myService() {
            return mock(MyServiceNoRetry.class);
        }
    }
}

@ActiveProfiles("Test")
@ContextConfiguration(classes = { SampleBatchApplication.class,
        MockBatchTestWithRetryVerificationFailures.MockedRetryService.class },
                      loader = AnnotationConfigContextLoader.class)
@RunWith(SpringRunner.class)
public class MockBatchTestWithRetryVerificationFailures {

    @Autowired
    MyRetryService service;

    @Test
    public void batchTest() {
        service.process();

        verify(service).process();
        validateMockitoUsage();
    }

    public static class MockedRetryService {
        @Bean
        @Primary
        public MyRetryService myRetryService() {
            return mock(MyRetryService.class);
        }
    }
}

编辑:根据我放在一起显示问题的示例项目更新了问题和代码。

【问题讨论】:

  • 你能更新你的代码吗
  • @kuhajeyan 当然,我已经做到了。希望我已经包含了足够的信息
  • @kuhajeyan 再次更新,但您可以尝试在 github 中使用工作示例
  • thx 会检查这个

标签: spring mockito spring-batch spring-aop spring-retry


【解决方案1】:

所以在查看了类似的github issue 为spring-boot 之后

我发现有一个额外的代理妨碍了。我通过手动解开 aop 类发现了一个讨厌的 hack,使验证工作,即:

@Test
public void batchTest() throws Exception {
    service.process();

    if (service instanceof Advised) {
        service = (MyRetryService) ((Advised) service).getTargetSource().getTarget();
    }

    verify(service).process();
    validateMockitoUsage();
}

希望这可以像上面的 github 问题一样得到解决。我会提出一个问题,看看我能走多远。

编辑:提出github issue

【讨论】:

【解决方案2】:

在我看到@Joel Pearsons 的答案,尤其是链接的 GitHub 问题之后,我临时使用了一个解包和验证的静态帮助方法来解决这个问题:

public static <T> T unwrapAndVerify(T mock, VerificationMode mode) {
    return ((T) Mockito.verify(AopTestUtils.getTargetObject(mock), mode));
}

使用此方法,测试用例的唯一区别是验证调用。除此之外没有其他开销:

unwrapAndVerify(service, times(2)).process();

而不是

verify(service, times(2)).process();

实际上,甚至可以将辅助方法命名为实际的 Mockito 方法,这样你只需要替换导入,但我不喜欢随后的混乱。

但是,如果使用 @MockBean 而不是 mock() 创建模拟 bean,则不需要展开。 Spring Boot 1.4 支持这个注解。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-08-08
    • 1970-01-01
    • 2018-10-12
    • 1970-01-01
    • 2014-02-03
    相关资源
    最近更新 更多