Spring的@Retryable在运行JUnit测试时无效

16

我有这个测试:

@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;

}

但是,当我运行测试时,会抛出一个运行时异常,这让我相信它没有重试。我做错了吗?

4个回答

14
这是因为您没有使用SpringJUnitClassRunner。Mockito和您自己的类没有考虑到@Retryable注解,因此您需要依靠Spring的实现来实现这一点。但是您的测试并没有激活Spring。
以下是SpringJUnit4ClassRunner JavaDoc中的内容:
SpringJUnit4ClassRunner是JUnit的BlockJUnit4ClassRunner的自定义扩展,通过TestContextManager及其支持类和注解,提供Spring TestContext框架的功能给标准的JUnit测试。要使用此类,只需将基于JUnit 4的测试类注释为@RunWith(SpringJUnit4ClassRunner.class)或@RunWith(SpringRunner.class)。
您应该将测试类重构为类似以下结构:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=MyConfig.class)
public class MyServiceTest {
    @Configuration
    @EnableRetry
    @Import(myService.class)
    public static class MyConfig {}
...

我在那里做什么?

  1. 激活Spring JUnit Hook
  2. 指定Spring上下文配置类
  3. 定义Spring配置并将服务导入为bean
  4. 启用可重试注释

还有其他需要注意的地方吗?

  • 是的,您正在使用Mockito模拟异常。如果您想像这样使用Spring测试此行为,则应查看Springockito Annotations
  • 但请注意:Springockito会完全替换Spring bean,这会强制您代理调用可重试的服务。您需要一个结构,例如:test -> retryableService -> exceptionThrowingBean。然后,您可以使用Springockito或任何您喜欢的工具,如ReflectionTestUtils,来配置exceptionThrowingBean以实现所需的行为。
  • 您应该在测试中引用服务的接口类型:MyServiceInterface
  • 最后但同样重要的是,几乎所有Java开发人员都遵循一种命名约定:类名的每个内部单词的第一个字母大写

希望对您有所帮助。


14

另一种方法:

@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();
    }

}

添加 @EnableRetry 工作了。 - sawan

4
我认为你应该让Spring来管理bean、创建适当的代理并处理过程。 如果你想mock特定的bean,你可以创建mocks并将它们注入到被测试的服务中。
第一种选择是取消代理服务,创建mocks并手动注入它们:
@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);
    }
}

在这个例子中,我模拟了一个名为RetryProperties的bean,并将其注入到服务中。此外,请注意,在这种方法中,您正在修改被Spring缓存的测试应用程序上下文。这意味着,如果您不使用@DirtiesContext,则服务将继续以模拟bean在其他测试中运行。您可以在这里阅读更多信息。
第二个选择是创建一个特定于测试的@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中。

0
这个解决方案对我有效:
Gradle:
dependencies {
    implementation 'org.junit.jupiter:junit-jupiter:5.9.0'
    implementation group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: '3.1.3'
    implementation group: 'org.springframework.retry', name: 'spring-retry', version: '2.0.2'
    implementation group: 'org.springframework', name: 'spring-aspects', version: '3.2.4.RELEASE'

    testImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-test', version: '3.1.3'
}

主要类别:
@SpringBootApplication
@EnableRetry
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

包含 @Retry 的类。
@Service
public class MyService {

    @Autowired
    private MyRepository myRepository;

    @Retryable(
            retryFor = {RetryException.class},
            maxAttempts = 3,
            backoff = @Backoff(delay = 500)
    )
    public String retryable() {
        return myRepository.call();
    }
}

测试:
@SpringBootTest
class MyControllerTest {

    @SpyBean
    private MyService myService;
    @MockBean
    private MyRepository myRepository;
    @Autowired
    private MyController sut;

    @Test
    void testRetry() {
        Mockito.when(myRepository.call())
                .thenThrow(new RetryException("Fail Retry 1"))
                .thenThrow(new RetryException("Fail Retry 2"))
                .thenReturn("Success!");

        sut.retry();

        Mockito.verify(myRepository, Mockito.times(3)).call();

    }

    @Test
    void testRetry2() {
        Mockito.when(myRepository.call())
                .thenThrow(new RetryException("Fail Retry 1"))
                .thenThrow(new RetryException("Fail Retry 2"))
                .thenThrow(new RetryException("Fail Retry 3"));

        RetryException exception = assertThrows(RetryException.class, () -> {
            sut.retry();
        });

        Mockito.verify(myRepository, Mockito.times(3)).call();
        assertEquals("Fail Retry 3", exception.getMessage());
    }

}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接