Keith Donald opened SPR-7551 and commented

I prefer to unit test my data access objects without a dependency on a Spring container. Since unit tests can require test-driven transactions, support for @Transactional test methods in this context would be preferable to working with the PlatformTransactionManager APIs.

I implemented this capability successfully using a JUnit MethodRule. For example:

public class JdbcAccountRepositoryTest {

    private EmbeddedDatabase db;

    private JdbcTemplate jdbcTemplate;

    private JdbcAccountRepository accountRepository;

    @Rule
    public TransactionalMethodRule transactional = new TransactionalMethodRule();

    @Before
    public void setup() {
        db = new GreenhouseTestDatabaseBuilder().member().connectedAccount().testData(getClass()).getDatabase();
        jdbcTemplate = new JdbcTemplate(db);
        accountRepository = new JdbcAccountRepository(jdbcTemplate, NoOpPasswordEncoder.getInstance(), new StubFileStorage(), "http://localhost:8080/members/{profileKey}");
    }

    @After
    public void destroy() {
        if (db != null) {
            db.shutdown();
        }
    }

    @Test
    @Transactional
        public void create() throws EmailAlreadyOnFileException {
        Person person = new Person("Jack", "Black", "jack@black.com", "foobie", Gender.Male, new LocalDate(1977, 12, 1));
        Account account = accountRepository.createAccount(person);
        assertEquals(3L, (long) account.getId());
        assertEquals("Jack Black", account.getFullName());
        assertEquals("jack@black.com", account.getEmail());
        assertEquals("http://localhost:8080/members/3", account.getProfileUrl());
        assertEquals("http://localhost:8080/resources/profile-pics/male/small.jpg", account.getPictureUrl());
    }
}

The MethodRule implementation:

package org.springframework.test.transaction;

import java.lang.reflect.Field;

import javax.sql.DataSource;

import org.junit.rules.MethodRule;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.Statement;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.util.ReflectionUtils;

public class TransactionalMethodRule implements MethodRule {

    public Statement apply(Statement base, FrameworkMethod method, Object target) {
        if (method.getAnnotation(Transactional.class) != null) {
            return new TransactionalStatement(base, target);
        } else {
            return base;
        }
    }

    private class TransactionalStatement extends Statement {

        private final Statement base;

        private final Object target;

        public TransactionalStatement(Statement base, Object target) {
            this.base = base;
            this.target = target;
        }

        @Override
        public void evaluate() throws Throwable {
            DataSource dataSource = getDataSource();
            PlatformTransactionManager tm = new DataSourceTransactionManager(dataSource);
            TransactionStatus txStatus = tm.getTransaction(new DefaultTransactionDefinition());
            try {
                base.evaluate();
            } catch (Throwable e) {
                tm.rollback(txStatus);
                throw e;
            }
            tm.commit(txStatus);
        }

        private DataSource getDataSource() {
            Field field = ReflectionUtils.findField(target.getClass(), "db");
            field.setAccessible(true);
            return (DataSource) ReflectionUtils.getField(field, target);
        }   

    }
}

Affects: 3.0.4

Issue Links: - #11259 Introduce a TestExecutionListener for DbUnit

1 votes, 2 watchers

Comment From: spring-projects-issues

Dave Syer commented

How does the @Rule know what transaction manager to use? Don't you need to at least pass in a DataSource?