jbehave-junit-runner
jbehave-junit-runner copied to clipboard
Spring @Transactional annotation was ignored by runners
Firstly, thank you for your very useful library! It helps me a lot for working with Spring, JBehave and JUnit. I got an issue when I tried to use Spring @Transactional annotation (to rollback transaction automatically): by using 'SpringJUnitReportingRunner', everything worked well but transaction didn't rollback. Then, I changed to use 'SpringJUnit4ClassRunner', run test again. After test done, I could saw transaction rollback as I expected, but very poor result in JUnit view :(. I was wonder if maybe Spring @Transactional annotation was ignored by classes: SpringJUnitReportingRunner/ JUnitReportingRunner?
Well, yes, you're right. The SpringJUnit4ClassRunner uses three test execution listeners:
-
org.springframework.test.context.support.DependencyInjectionTestExecutionListener
-
org.springframework.test.context.support.DirtiesContextTestExecutionListener
-
org.springframework.test.context.transaction.TransactionalTestExecutionListener
These default test execution listeners are defined in org.springframework.test.context.TestContextManager
. The SpringJUnitReportingRunner
only calls prepareTestInstance()
on the TextContextManager
instance which delegates the call to the test execution listeners and this method is not overridden by the TransactionalTestExecutionListener
. TransactionalTestExecutionListener
's magic happens in beforeTestMethod()
and afterTestMethod()
but the SpringJUnitReportingRunner
is not using them.
@visusnet
Thank you for your quick response, very detailed explanation :blush:
I know JBehave
(and general BDD implementations) focus on acceptance tests, so it's often unnecessary to support transaction rollback. But in some cases, it is required to rollback transaction after each test for clean data.
By your given information, I tried to find a solution that could make SpringJUnitReportingRunner
works well with Spring Transactional
annotation. Lucky I found one, the following is my snippets:
- JUnit-runnable entry-point to run multiple stories: NewContactServiceBDDTest.java
:information_source: override run() method with addition of Spring Transactional
annotation
@RunWith(SpringJUnitReportingRunner.class)
@ContextConfiguration("file:src/main/webapp/WEB-INF/spring-servlet.xml")
public class NewContactServiceBDDTest extends JUnitStories {
@Autowired
private ApplicationContext context;
@Override
public Configuration configuration() {
return new MostUsefulConfiguration();
}
public InjectableStepsFactory stepsFactory() {
return new SpringStepsFactory(configuration(), context);
}
@Override
protected List<String> storyPaths() {
return Arrays.asList("net/viralpatel/contact/service/add_contact.story");
}
@Transactional
public void run() throws Throwable {
super.run();
}
}
- Modified runner: SpringJUnitReportingRunner.java
:information_source: override JUnitReportingRunner.run(RunNotifier notifier) method, add new org.junit.runner.notification.RunListener
to notifier. It will invoke org.springframework.test.context.TestContextManager
beforeTestMethod(...) and afterTestMethod(...) to fire TransactionalTestExecutionListener
execution
public class SpringJUnitReportingRunner extends JUnitReportingRunner {
SpringJunit4ClassRunnerExecutor executor;
ConfigurableEmbedder ce;
public SpringJUnitReportingRunner(Class<? extends ConfigurableEmbedder> testClass) throws Throwable {
super(testClass);
}
@Override
protected void prepareConfigurableEmbedder(Class<? extends ConfigurableEmbedder> testClass, ConfigurableEmbedder configurableEmbedder) throws Exception {
ce = configurableEmbedder;
executor = new SpringJunit4ClassRunnerExecutor(testClass);
executor.getHiddenTestContextManager().prepareTestInstance(configurableEmbedder);
}
@Override
public void run(RunNotifier notifier) {
notifier.addListener(new RunListener() {
public void testRunStarted(Description description) throws Exception {
System.err.println("testRunStarted");
executor.getHiddenTestContextManager().beforeTestMethod(ce, description.getTestClass().getDeclaredMethod("run", new Class[] {}));
}
public void testRunFinished(Result result) throws Exception {
System.err.println("testRunFinished");
executor.getHiddenTestContextManager().afterTestMethod(ce, ce.getClass().getDeclaredMethod("run", new Class[] {}), new Throwable("error"));
}
});
super.run(notifier);
}
private static class SpringJunit4ClassRunnerExecutor extends SpringJUnit4ClassRunner {
public SpringJunit4ClassRunnerExecutor(Class<?> clazz) throws InitializationError {
super(clazz);
}
public TestContextManager getHiddenTestContextManager() {
return getTestContextManager();
}
}
}
-
Important: To support Spring transaction rollback, must change execution mechanism of
org.jbehave.core.embedder.StoryManager
to SEQUENCE (it means we won't usejava.util.concurrent.ExecutorService
to run stories concurrency, but one by one) - Now, everything works well, transaction rollback automatically after each test/ stories :relaxed:
The above snippets are only my draft, I know it's not a good solution, just makes JBehave
, Spring
and JUnit
work together, Could you please investigate and make it better? I believe you can do it :+1:
I will follow you!
Wow, that surely looks like a promising approach. However, there are some drawbacks (for example: usage of reflection, the necessary manual override of the run()
method in a class that inherits from JUnitStories
, instantiating Throwable
). I think the additional RunListener
is the important step. What's bothering me most is the fact, that you need to provide a run method that is annotated with @Transactional
but then again: You need to state somewhere that you want to enable transaction support (maybe on class level?).
During further investigation, I noticed that a @Transactional
annotation on class level should work. Here is why:
- The
TransactionalTestExecutionListener
uses an instance ofAnnotationTransactionAttributeSource
to get information about transactions (rollback strategy, transaction: yes/no, a.s.o.). See for example: http://grepcode.com/file/repo1.maven.org/maven2/org.springframework/spring-test/3.0.1.RELEASE/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java#131 - The
AnnotationTransactionAttributeSource
has a list of annotation parsers. One element of this list is an instance ofSpringTransactionAnnotationParser
. Somewhere deep within the execution, you should end up here: http://grepcode.com/file/repo1.maven.org/maven2/org.springframework/spring-tx/3.0.1.RELEASE/org/springframework/transaction/interceptor/AbstractFallbackTransactionAttributeSource.java#150. - So basically, if there is no
@Transactional
on the method, the transaction mechanism tries to detect an annotation on class level within the call ofbeforeTestMethod()
.
Unfortunately, I don't see a way around the reflection magic... Mhmm...