Highly Opinionated Thoughts on Programming

by Elnur Abdurrakhimov


Programmatic Transaction Management in Tests With Spring

Aug 9, 2014


Spring supports transactions during tests, but those transactions either roll back or commit only after a test has finished running. So what if we need to commit a transaction during the test? That might be necessary if we’re preparing test fixtures for each test inside tests themselves — not using some global fixtures that all the tests share.

Here’s an example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class QuestionApiTest {
    @Autowired
    private RequestRepository requestRepository;
    @Autowired
    private QuestionRepository questionRepository;

    @Test
    public void listByRequest() throws Exception {
        Request request = requestRepository.save(aRequest().build());
        Question firstQuestion = questionRepository.save(aQuestion()
            .withRequest(request)
            .build());
        Question secondQuestion = questionRepository.save(aQuestion()
            .withRequest(request)
            .build());

        // Doing an HTTP request to the app to get a list of both questions
        // found by the given request
    }
}

The reason we have to run these statements in a transaction in the first place is that without a transaction we’d get an exception:

org.hibernate.PersistentObjectException: detached entity passed to persist: com.example.model.Request

It happens because after the request has been persisted, the JPA session closes and request ends up detached. Hence we need to run all three statements in a transaction.

In the test above we have to commit the transaction before we do an HTTP request to the app because otherwise the app won’t see the entities we’ve persisted. It happens because the application will have a separate transaction and the app’s transaction won’t see the data changes done during the test because they haven’t been committed yet.

To solve the problem we need to start and commit our own transaction during the test. However if we try injecting EntityManager and using it to handle a transaction directly, we’ll get another exception:

java.lang.IllegalStateException: Not allowed to create transaction on shared EntityManager - use Spring transactions or EJB CMT instead

That happens because Spring manages transactions for us. For that we have enabled transaction management and defined a PlatformTransactionManager bean:

1
2
3
4
5
6
7
8
9
10
@Configuration
@EnableTransactionManagement
public class PersistenceConfig {
    @Bean
    public PlatformTransactionManager transactionManager() {
        return new JpaTransactionManager(entityManagerFactory());
    }
    
    // other bean definitions
}

Hence, to solve the problem, we need to use Spring’s transaction facilities instead. Here’s how we can do it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class QuestionApiTest {
    @Autowired
    private RequestRepository requestRepository;
    @Autowired
    private QuestionRepository questionRepository;
    @Autowired
    private PlatformTransactionManager transactionManager;

    @Test
    public void listByRequest() throws Exception {
        DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
        TransactionStatus transaction = transactionManager.getTransaction(definition);

        Request request = requestRepository.save(aRequest().build());
        Question firstQuestion = questionRepository.save(aQuestion()
            .withRequest(request)
            .build());
        Question secondQuestion = questionRepository.save(aQuestion()
            .withRequest(request)
            .build());

        transactionManager.commit(transaction);

        // Doing an HTTP request to the app to get a list of both questions
        // found by the given request
    }
}

Finally, we can persist several entities in a single transaction and avoid the problem with a detached entity.

But we can go a step further and abstract transaction plumbing away. First, we create the Transactor class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Component
public class Transactor {
    @Autowired
    private PlatformTransactionManager transactionManager;

    public void perform(UnitOfWork unitOfWork) {
        DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
        TransactionStatus transaction = transactionManager.getTransaction(definition);

        try {
            unitOfWork.work();
            transactionManager.commit(transaction);
        } catch (Exception e) {
            transactionManager.rollback(transaction);
            throw e;
        }
    }
}

Our Transactor needs another class — UnitOfWork:

1
2
3
public interface UnitOfWork {
    void work();
}

Here’s how our test looks like now:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class QuestionApiTest {
    @Autowired
    private RequestRepository requestRepository;
    @Autowired
    private QuestionRepository questionRepository;
    @Autowired
    private Transactor transactor;

    @Test
    public void listByRequest() throws Exception {
        transactor.perform(new UnitOfWork() {
            @Override
            public void work() {
                Request request = requestRepository.save(aRequest().build());
                Question firstQuestion = questionRepository.save(aQuestion()
                    .withRequest(request)
                    .build());
                Question secondQuestion = questionRepository.save(aQuestion()
                    .withRequest(request)
                    .build());
            }
        });

        // Doing an HTTP request to the app to get a list of both questions
        // found by the given request
    }
}

And if we’re using Java 8, we can be more succinct with a lambda:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class QuestionApiTest {
    @Autowired
    private RequestRepository requestRepository;
    @Autowired
    private QuestionRepository questionRepository;
    @Autowired
    private Transactor transactor;

    @Test
    public void listByRequest() throws Exception {
        transactor.perform(() -> {
            Request request = requestRepository.save(aRequest().build());
            Question firstQuestion = questionRepository.save(aQuestion()
                .withRequest(request)
                .build());
            Question secondQuestion = questionRepository.save(aQuestion()
                .withRequest(request)
                .build());
        });

        // Doing an HTTP request to the app to get a list of both questions
        // found by the given request
    }
}


© Elnur Abdurrakhimov