我编写了一个 Spock 测试来使用 Spring-retry 来执行服务,但测试上下文未以某种方式正确设置。
具体错误是(@kriegaex插入的换行符):
[ERROR]
IntegrationTestSpec.test success the first time:25 »
NullPointer Cannot invoke "org.springframework.retry.RetryContext.getRetryCount()"
because the return value of
"org.springframework.retry.support.RetrySynchronizationManager.getContext()"
is null
有一个上一个问题询问相同的错误,但没有提供重现该问题的代码,因此它没有走得太远。这个问题的完整代码如下,也位于 https://github.com/nathan-hughes/spring-retry-examples。
失败的测试:
package ndhtest
import spock.lang.Specification
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.beans.factory.annotation.Autowired
import org.spockframework.spring.SpringBean
import spock.lang.Subject
@SpringBootTest(classes = [MyRetryableService, RandomNumberService])
class IntegrationTestSpec extends Specification {
@SpringBean
RandomNumberService randomNumberService = Stub(RandomNumberService)
@Subject
@Autowired
MyRetryableService myRetryableService
def "test success the first time"() {
given:
randomNumberService.randomNumber() >> 1
when:
int result = myRetryableService.doStuff('hello')
then:
result == 1
}
}
要测试的代码:
package ndhtest;
import lombok.extern.slf4j.Slf4j;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.retry.annotation.EnableRetry;
@SpringBootApplication
@EnableRetry
@Slf4j
@RequiredArgsConstructor
public class SpringBootConsoleApplication implements CommandLineRunner {
private final MyRetryableService myRetryableService;
public static void main(String ... args) throws Exception {
log.info("starting application");
SpringApplication.run( SpringBootConsoleApplication.class);
log.info("finishing application");
}
@Override
public void run(String ... args) {
log.info("executing command line runner");
try {
int i = myRetryableService.doStuff("hello");
log.info("myRetryableService returned {}", i);
} catch (Exception e) {
log.error("caught exception", e);
}
}
}
执行重试的服务,它具有获取重试计数的日志记录,这在 Spock 测试之外运行代码时有效:
package ndhtest;
import java.util.Random;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.retry.annotation.Retryable;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.support.RetrySynchronizationManager;
import org.springframework.stereotype.Service;
@Service
@Slf4j
@RequiredArgsConstructor
public class MyRetryableService {
private final RandomNumberService randomNumberService;
@Retryable(value = {RetryableException.class}, maxAttempts = 2, backoff = @Backoff(delay=100))
public int doStuff(String s) {
log.info("doing stuff with {}, try = {}", s, RetrySynchronizationManager.getContext().getRetryCount() + 1);
int i = randomNumberService.randomNumber();
// simulate having something bad happen that is recoverable
if (i % 2 == 0) {
String issue = "oops";
log.warn("condition = {}", issue);
throw new RetryableException(issue);
}
// simulate having something bad happen that is not recoverable
if (i % 5 == 0) {
String issue = "ohnoes";
log.warn("condition = {}", issue);
throw new IllegalArgumentException(issue);
}
return i;
}
@Recover
public int recover(RetryableException e, String s) {
log.info("in recover for RetryableException, s is {}", s);
return -1;
}
@Recover
public int recover(RuntimeException e, String s) {
log.info("in recover for RuntimeException, s is {}", s);
throw e;
}
}
配角:
package ndhtest;
import java.util.Random;
import org.springframework.stereotype.Service;
@Service
public class RandomNumberService {
private Random random = new Random();
public int randomNumber() {
return random.nextInt();
}
}
package ndhtest;
public class RetryableException extends RuntimeException {
public RetryableException(String message, Throwable throwable) {
super(message, throwable);
}
public RetryableException(String message) {
this(message, null);
}
}
pom 文件:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.6</version>
<relativePath/>
</parent>
<groupId>ndhtest</groupId>
<artifactId>spring-retry-example</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<java.version>17</java.version>
<groovy.version>4.0.5</groovy.version>
<springboot.version>3.1.6</springboot.version>
<lombok.version>1.18.30</lombok.version>
<spock.version>2.4-M1-groovy-4.0</spock.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-core</artifactId>
<version>${spock.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-spring</artifactId>
<version>${spock.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.groovy</groupId>
<artifactId>groovy</artifactId>
<version>${groovy.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${springboot.version}</version>
<configuration>
<mainClass>ndhtest.SpringBootConsoleApplication</mainClass>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.gmavenplus</groupId>
<artifactId>gmavenplus-plugin</artifactId>
<version>2.0.0</version>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compileTests</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M7</version>
<configuration>
<useModulePath>false</useModulePath> <!-- https://issues.apache.org/jira/browse/SUREFIRE-1809 -->
<useFile>false</useFile>
<includes>
<include>**/*Test</include>
<include>**/*Spec</include>
</includes>
<statelessTestsetReporter implementation="org.apache.maven.plugin.surefire.extensions.junit5.JUnit5Xml30StatelessReporter">
<disable>false</disable>
<version>3.0</version>
<usePhrasedFileName>false</usePhrasedFileName>
<usePhrasedTestSuiteClassName>true</usePhrasedTestSuiteClassName>
<usePhrasedTestCaseClassName>true</usePhrasedTestCaseClassName>
<usePhrasedTestCaseMethodName>true</usePhrasedTestCaseMethodName>
</statelessTestsetReporter>
</configuration>
</plugin>
</plugins>
</build>
至少有两种方法可以实现此目的:
使用
@SpringBootTest
,不带 classes
参数。
在测试开始时添加
RetrySynchronizationManager.register(Mock(RetryContext))
,如您还链接到的其他问题中here的建议。然后,您可以继续在classes
中使用@SpringBootTest
。