I have an application that uses persistent JMS queues. Imagine I have an application failure after reading a message and before ack'ing it. The persistent queue must provide that message again after the app restarts. How can I implement a junit integration test for this? I'm testing application restart after a (simulated) application crash mid-"transaction".
I've looked at @DirtiesContext as a way to reset all the Spring parts of the app: reading configs, recreating JMS connections. I could have one test case A) write a message, allow the message to be read and then "exit" (shut down the spring context?) without acking. Then another test case (after the context is reloaded B) read the message and assert that it was not lost after the simulated application restart. But the builtin context reload provided by @DirtiesContext only happens between test cases. And JUnit does not provide for a means to sequence two test cases or make B) dependent on A), such that A) will always run (and run first) if you decide to run B).
In a previous life, I wrote manual code that shut down the spring context, and manually restarted a new context. E.g. between A) and B). That could be done within a single test case. It wouldn't play nicely with @RunWith(SpringRunner.class), I'm guessing, and seems pretty old school. Is that really the only option, given all the wonderful Spring and JUnit support these days?
This seems like a pretty useful technique. It could be used to test re-arrival of messages after they've been rolled back (and are stuck on a dead letter queue); or that sequence numbers written to a DB are really persisting during a "crash". Any number of failure cases that wind up affecting the next application startup due to persisted (or not) data. How do we simulate spring restart in junit tests? Either within one test, or create a sequence of dependent tests with @DirtiesContext between them.
The following article How to Restart a Spring Application Context within a JUnit test describes a solution to the question.
The solution consist to expend SpringJUnit4ClassRunner in order to inject a SpringRestarter singleton which takes care of restating the application context.
public class SpringRestarter {
private static SpringRestarter INSTANCE = null;
private TestContextManager testContextManager;
public static SpringRestarter getInstance() {
if (INSTANCE == null) {
INSTANCE = new SpringRestarter();
}
return INSTANCE;
}
public void init(TestContextManager testContextManager) {
this.testContextManager = testContextManager;
}
public void restart(Runnable stoppedLogic) {
testContextManager.getTestContext().markApplicationContextDirty(DirtiesContext.HierarchyMode.EXHAUSTIVE);
if (stoppedLogic != null) {
stoppedLogic.run();
}
testContextManager.getTestContext().getApplicationContext();
reinjectDependencies();
}
private void reinjectDependencies() {
testContextManager
.getTestExecutionListeners()
.stream()
.filter(listener -> listener instanceof DependencyInjectionTestExecutionListener)
.findFirst()
.ifPresent(listener -> {
try {
listener.prepareTestInstance(testContextManager.getTestContext());
} catch (Exception e) {
e.printStackTrace();
}
});
}
}
RestartingSpringJUnit4ClassRunner extends SpringJUnit4ClassRunner and initialise the singleton SpringRestarter.
public class RestartingSpringJUnit4ClassRunner extends SpringJUnit4ClassRunner {
public RestartingSpringJUnit4ClassRunner(Class<?> clazz) throws InitializationError {
super(clazz);
}
@Override
protected TestContextManager createTestContextManager(Class<?> clazz) {
final TestContextManager testContextManager = super.createTestContextManager(clazz);
SpringRestarter.getInstance().init(testContextManager);
return testContextManager;
}
}
RestartingSpringRunner extends RestartingSpringJUnit4ClassRunner in order to extend SpringRunner
public class RestartingSpringRunner extends RestartingSpringJUnit4ClassRunner {
public RestartingSpringRunner(Class<?> clazz) throws InitializationError {
super(clazz);
}
}
Using Within a JUnit 4 Test
@RunWith(RestartingSpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class MyIntegrationTests {
public void myRestartingTest() {
//Some test logic before the context restart
SpringRestarter.getInstance().restart(() -> {/* Some logic after context stopped */});
//Some test logic after the context restart
}
}
Full details of the explanation
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With