jbehave-junit-runner icon indicating copy to clipboard operation
jbehave-junit-runner copied to clipboard

Spring @Transactional annotation was ignored by runners

Open tuannm-sou opened this issue 10 years ago • 3 comments

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?

tuannm-sou avatar Jun 14 '14 13:06 tuannm-sou

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 avatar Jun 15 '14 13:06 visusnet

@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 use java.util.concurrent.ExecutorService to run stories concurrency, but one by one)
  • Now, everything works well, transaction rollback automatically after each test/ stories :relaxed:

capture_01

capture_02

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!

tuannm-sou avatar Jun 16 '14 10:06 tuannm-sou

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 of AnnotationTransactionAttributeSource 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 of SpringTransactionAnnotationParser. 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 of beforeTestMethod().

Unfortunately, I don't see a way around the reflection magic... Mhmm...

visusnet avatar Jun 16 '14 11:06 visusnet