Spring's @Retryable not working when running JUnit Test

霸气de小男生 提交于 2020-05-29 03:23:53

问题


I have this 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();
}

and then I have:

@Service("myService")
public class myService extends myAbstractServiceClass {


public String print1() {
    String temp = "";
    temp = print2();
    return temp;
}

 @Retryable
 public String print2() {
     return "completed";
 }
}

then I have this interface(which my abstractService implements):

public interface myServiceInterface {

    @Retryable(maxAttempts = 3)
    String print1() throws RuntimeException;

    @Retryable(maxAttempts = 3)
    String print2() throws RuntimeException;

}

but, I get a runtimeexception thrown when I run the test, leading me to believe it is not retrying. Am I doing this wrong?


回答1:


This is because you are not using the SpringJUnitClassRunner.

Mockito and your own classes are not taking the @Retryable annotation in account. So you rely on the implementation of Spring to do so. But your test does not activate Spring.

This is from the SpringJUnit4ClassRunner JavaDoc:

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).

You should restructure your test class at least to something like:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=MyConfig.class)
public class MyServiceTest {
    @Configuration
    @EnableRetry
    @Import(myService.class)
    public static class MyConfig {}
...

What am I doing there?

  1. activate the Spring JUnit hook
  2. specify the Spring context configuration class
  3. define the spring configuration and import your service as a bean
  4. enable the retryable annotation

Are there some other pitfalls?

  • Yes, you are using Mockito to simulate an exception. If you want to test this behaviour with Spring like this, you should have a look at Springockito Annotations.
  • But be aware of that: Springockito you will replace the spring bean completely which forces you to proxy the call of your retryable. You need a structure like: test -> retryableService -> exceptionThrowingBean. Then you can use Springockito or what ever you like e.g. ReflectionTestUtils to configure the exceptionThrowingBean with the behaviour you like.
  • You should reference the interface type of your service in your test: MyServiceInterface
  • And last but not least. There is a naming convention nearly all Java developers follow: class names have first letter of each internal word capitalized

Hope that helps.




回答2:


I think you should let Spring manage the bean, create the appropriate proxy and handle the process. If you want to mock specific beans, you can create mocks and inject them to the service under test.

1st option could be unwrapping proxied service, creating mocks and manually injecting them:

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

In this example, I mocked one bean, RetryProperties, and injected into the service. Also note that, in this approach you are modifying the test application context which is cached by Spring. This means that if you don't use @DirtiesContext, service will continue its way with mocked bean in other tests. You can read more here

Second option would be creating a test specific @Configuration and mock the depended bean there. Spring will pick up this new mocked bean instead of the original one:

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

In this example, we have defined a test specific configuration and added it to the @ContextConfiguration.




回答3:


Another way:

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

}


来源:https://stackoverflow.com/questions/39478665/springs-retryable-not-working-when-running-junit-test

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!