问题
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?
- activate the Spring JUnit hook
- specify the Spring context configuration class
- define the spring configuration and import your service as a bean
- 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 theexceptionThrowingBean
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