2

私はSpring Boot1.4.0.RELEASEを使用していますspring-boot-starter-batch.spring-boot-starter-aopspring-retry

@Service実行時にモックされる Spring Integration テストがあります。@Serviceクラスの@Retryableメソッドに注釈が含まれている場合、 と干渉しているように見えることに気付きましMockito.verify()UnfinishedVerificationException。これは何か関係があるに違いないと思いspring-aopますか?@Retryableのすべての注釈をコメントアウトすると、正常に@Service動作することを確認できます。

この問題を示すgithub プロジェクトを作成しました。

で失敗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);
        }
    }
}

編集:問題を示すためにまとめたサンプルプロジェクトに基づいて、質問とコードを更新しました。

4

2 に答える 2

4

同様のgithubの問題を見た後spring-boot

余分なプロキシが邪魔をしていることがわかりました。aop クラスを手でアンラップして、検証作業を行う厄介なハックを見つけました。つまり、次のようになります。

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

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

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

うまくいけば、これは上記の github の問題と同様に修正できます。問題を提起して、どこまで到達するかを確認します。

編集:githubの問題を提起しました

于 2016-09-07T03:25:42.563 に答える
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 メソッドのようにヘルパー メソッドに名前を付けることもできたので、インポートを置き換えるだけで済みましたが、その後の混乱は気に入りませんでした。

ただし、モック Bean を作成する@MockBean代わりに を使用する場合は、ラップ解除は必要ありません。mock()Spring Boot 1.4 は、このアノテーションをサポートしています。

于 2016-11-07T10:50:50.277 に答える