Spring 的 @Retryable 在 运行 JUnit 测试时不起作用
Spring's @Retryable not working when running JUnit Test
我有这个测试:
@RunWith(MockitoJUnitRunner.class)
public class myServiceTest {
@InjectMocks
myService subject;
private myService spy;
@Before
public void before() {
spy = spy(subject);
}
@Test
public void testing() {
when(spy.print2()).thenThrow(new RuntimeException()).thenThrow(new RuntimeException()).thenReturn("completed");
spy.print1();
verify(spy, times(3)).print2();
}
然后我有:
@Service("myService")
public class myService extends myAbstractServiceClass {
public String print1() {
String temp = "";
temp = print2();
return temp;
}
@Retryable
public String print2() {
return "completed";
}
}
然后我有这个接口(我的 abstractService 实现):
public interface myServiceInterface {
@Retryable(maxAttempts = 3)
String print1() throws RuntimeException;
@Retryable(maxAttempts = 3)
String print2() throws RuntimeException;
}
但是,当我 运行 测试时抛出一个 运行timeexception,这让我相信它不会重试。我做错了吗?
这是因为您没有使用 SpringJUnitClassRunner
。
Mockito 和您自己的 classes 没有考虑 @Retryable
注释。所以你依靠 Spring 的实现来做到这一点。但是你的测试没有激活Spring。
这来自 SpringJUnit4ClassRunner Java文档:
SpringJUnit4ClassRunner is a custom extension of JUnit's BlockJUnit4ClassRunner which provides functionality of the Spring TestContext Framework to standard JUnit tests by means of the TestContextManager and associated support classes and annotations.
To use this class, simply annotate a JUnit 4 based test class with @RunWith(SpringJUnit4ClassRunner.class) or @RunWith(SpringRunner.class).
你应该重组你的测试 class 至少像这样:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=MyConfig.class)
public class MyServiceTest {
@Configuration
@EnableRetry
@Import(myService.class)
public static class MyConfig {}
...
我在那里做什么?
- 激活 Spring JUnit 挂钩
- 指定Spring上下文配置class
- 定义 spring 配置并将您的服务作为 bean 导入
- 启用可重试注释
还有其他陷阱吗?
- 是的,您正在使用 Mockito 来模拟异常。如果你想像这样用 Spring 测试这种行为,你应该看看 Springockito Annotations.
- 但请注意:Springockito 您将完全替换 spring bean,这会强制您代理可重试的调用。您需要这样的结构:
test -> retryableService -> exceptionThrowingBean
。然后你可以使用 Springockito 或者你喜欢的任何东西,例如ReflectionTestUtils
以您喜欢的行为配置 exceptionThrowingBean
。
- 您应该在测试中参考您服务的接口类型:
MyServiceInterface
- 最后但并非最不重要的一点。几乎所有 Java 开发人员都遵循一个命名约定:class 名称有
first letter of each internal word capitalized
希望对您有所帮助。
我认为您应该让 Spring 管理 bean,创建适当的代理并处理进程。
如果你想模拟特定的bean,你可以创建模拟并将它们注入到被测服务中。
第一个选项可能是解包代理服务,创建模拟并手动注入它们:
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = {RetryConfiguration.class})
@DirtiesContext
public class TheServiceImplTest {
@Autowired
private TheService theService;
@Before
public void setUp(){
TheService serviceWithoutProxy = AopTestUtils.getUltimateTargetObject(theService);
RetryProperties mockRetryProperties = Mockito.mock(RetryProperties.class);
ReflectionTestUtils.setField(serviceWithoutProxy, "retryProperties", mockRetryProperties);
}
@Test
public void shouldFetch() {
Assert.assertNotNull(theService);
}
}
在这个例子中,我模拟了一个 bean RetryProperties,并注入到服务中。另请注意,在这种方法中,您正在修改由 Spring 缓存的测试应用程序上下文。这意味着如果您不使用@DirtiesContext,服务将在其他测试中继续使用模拟 bean。你可以阅读更多here
第二个选项是创建一个特定于测试的@Configuration 并在那里模拟依赖的bean。 Spring 将选择这个新的模拟 bean 而不是原来的 bean:
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = {RetryConfiguration.class, TheServiceImplSecondTest.TestConfiguration.class})
public class TheServiceImplSecondTest {
@Autowired
private TheService theService;
@Test
public void shouldFetch() {
Assert.assertNotNull(theService);
}
@Configuration
static class TestConfiguration {
@Bean
public RetryProperties retryProperties() {
return Mockito.mock(RetryProperties.class);
}
}
}
在这个例子中,我们定义了一个特定于测试的配置并将其添加到@ContextConfiguration。
另一种方式:
@EnableRetry
@RunWith(SpringRunner.class)
@SpringBootTest(classes={ServiceToTest.class})
public class RetryableTest {
@Autowired
private ServiceToTest serviceToTest;
@MockBean
private ComponentInsideTestClass componentInsideTestClass;
@Test
public void retryableTest(){
serviceToTest.method();
}
}
我有这个测试:
@RunWith(MockitoJUnitRunner.class)
public class myServiceTest {
@InjectMocks
myService subject;
private myService spy;
@Before
public void before() {
spy = spy(subject);
}
@Test
public void testing() {
when(spy.print2()).thenThrow(new RuntimeException()).thenThrow(new RuntimeException()).thenReturn("completed");
spy.print1();
verify(spy, times(3)).print2();
}
然后我有:
@Service("myService")
public class myService extends myAbstractServiceClass {
public String print1() {
String temp = "";
temp = print2();
return temp;
}
@Retryable
public String print2() {
return "completed";
}
}
然后我有这个接口(我的 abstractService 实现):
public interface myServiceInterface {
@Retryable(maxAttempts = 3)
String print1() throws RuntimeException;
@Retryable(maxAttempts = 3)
String print2() throws RuntimeException;
}
但是,当我 运行 测试时抛出一个 运行timeexception,这让我相信它不会重试。我做错了吗?
这是因为您没有使用 SpringJUnitClassRunner
。
Mockito 和您自己的 classes 没有考虑 @Retryable
注释。所以你依靠 Spring 的实现来做到这一点。但是你的测试没有激活Spring。
这来自 SpringJUnit4ClassRunner Java文档:
SpringJUnit4ClassRunner is a custom extension of JUnit's BlockJUnit4ClassRunner which provides functionality of the Spring TestContext Framework to standard JUnit tests by means of the TestContextManager and associated support classes and annotations. To use this class, simply annotate a JUnit 4 based test class with @RunWith(SpringJUnit4ClassRunner.class) or @RunWith(SpringRunner.class).
你应该重组你的测试 class 至少像这样:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=MyConfig.class)
public class MyServiceTest {
@Configuration
@EnableRetry
@Import(myService.class)
public static class MyConfig {}
...
我在那里做什么?
- 激活 Spring JUnit 挂钩
- 指定Spring上下文配置class
- 定义 spring 配置并将您的服务作为 bean 导入
- 启用可重试注释
还有其他陷阱吗?
- 是的,您正在使用 Mockito 来模拟异常。如果你想像这样用 Spring 测试这种行为,你应该看看 Springockito Annotations.
- 但请注意:Springockito 您将完全替换 spring bean,这会强制您代理可重试的调用。您需要这样的结构:
test -> retryableService -> exceptionThrowingBean
。然后你可以使用 Springockito 或者你喜欢的任何东西,例如ReflectionTestUtils
以您喜欢的行为配置exceptionThrowingBean
。 - 您应该在测试中参考您服务的接口类型:
MyServiceInterface
- 最后但并非最不重要的一点。几乎所有 Java 开发人员都遵循一个命名约定:class 名称有
first letter of each internal word capitalized
希望对您有所帮助。
我认为您应该让 Spring 管理 bean,创建适当的代理并处理进程。 如果你想模拟特定的bean,你可以创建模拟并将它们注入到被测服务中。
第一个选项可能是解包代理服务,创建模拟并手动注入它们:
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = {RetryConfiguration.class})
@DirtiesContext
public class TheServiceImplTest {
@Autowired
private TheService theService;
@Before
public void setUp(){
TheService serviceWithoutProxy = AopTestUtils.getUltimateTargetObject(theService);
RetryProperties mockRetryProperties = Mockito.mock(RetryProperties.class);
ReflectionTestUtils.setField(serviceWithoutProxy, "retryProperties", mockRetryProperties);
}
@Test
public void shouldFetch() {
Assert.assertNotNull(theService);
}
}
在这个例子中,我模拟了一个 bean RetryProperties,并注入到服务中。另请注意,在这种方法中,您正在修改由 Spring 缓存的测试应用程序上下文。这意味着如果您不使用@DirtiesContext,服务将在其他测试中继续使用模拟 bean。你可以阅读更多here
第二个选项是创建一个特定于测试的@Configuration 并在那里模拟依赖的bean。 Spring 将选择这个新的模拟 bean 而不是原来的 bean:
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = {RetryConfiguration.class, TheServiceImplSecondTest.TestConfiguration.class})
public class TheServiceImplSecondTest {
@Autowired
private TheService theService;
@Test
public void shouldFetch() {
Assert.assertNotNull(theService);
}
@Configuration
static class TestConfiguration {
@Bean
public RetryProperties retryProperties() {
return Mockito.mock(RetryProperties.class);
}
}
}
在这个例子中,我们定义了一个特定于测试的配置并将其添加到@ContextConfiguration。
另一种方式:
@EnableRetry
@RunWith(SpringRunner.class)
@SpringBootTest(classes={ServiceToTest.class})
public class RetryableTest {
@Autowired
private ServiceToTest serviceToTest;
@MockBean
private ComponentInsideTestClass componentInsideTestClass;
@Test
public void retryableTest(){
serviceToTest.method();
}
}