Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Cannot test expected exception when using @Transactional with @Commit

I am using Spring Boot 1.4.0.M3.

I have an @Entity that has a username which should be unique:

@NotNull
@Column(unique = true)
@Pattern(regexp = "[a-zA-Z_\\-\\.0-9]+")
@Size(min = 1, max = 30)
private String username;

Using a Spring Data Repository, I want to test if there will be an exception when a duplicate username is used. This test works:

@SpringBootTest
@RunWith(SpringRunner.class)
public class UserRepositoryIntegrationTest {

    @Autowired
    private UserRepository repository;

    @Test(expected = DataIntegrityViolationException.class)
    public void test() {
        repository.save(User.createAdmin(repository.nextId(), "wim", "123456"));
        repository.save(User.createAdmin(repository.nextId(), "wim", "123456"));
    }
}

However, when adding @Transactional with @Commit, this test fails:

@SpringBootTest
@RunWith(SpringRunner.class)
@Transactional
public class UserRepositoryIntegrationTest {

    @Autowired
    private UserRepository repository;

    @Test(expected = DataIntegrityViolationException.class)
    @Commit
    public void test() {
        repository.save(User.createAdmin(repository.nextId(), "wim", "123456"));
        repository.save(User.createAdmin(repository.nextId(), "wim", "123456"));
    }
}

But looking at the logging, the DataIntegrityViolationException is being thrown:

2016-05-24 09:05:16.619 ERROR 22790 --- [ main] o.h.engine.jdbc.spi.SqlExceptionHelper : Unique index or primary key violation: "UK_D8HGQ87BS4VPMC81NQ9G69G8X_INDEX_D ON PUBLIC.MYPROJECT_USER(USERNAME) VALUES ('wim', 1)"; SQL statement: insert into myproject_user (password, username, role, id) values (?, ?, 'ADMIN', ?) [23505-191] 2016-05-24 09:05:16.620 INFO 22790 --- [
main] o.h.e.j.b.internal.AbstractBatchImpl : HHH000010: On release of batch it still contained JDBC statements 2016-05-24 09:05:16.629 WARN 22790 --- [ main] o.s.test.context.TestContextManager
: Caught exception while allowing TestExecutionListener [org.springframework.test.context.transaction.TransactionalTestExecutionListener@589b3632] to process 'after' execution for test: method [public void com.spring.boot.test.user.UserRepositoryIntegrationTest.test()], instance [com.spring.boot.test.user.UserRepositoryIntegrationTest@3ca278bc], exception [java.lang.AssertionError: Expected exception: org.springframework.dao.DataIntegrityViolationException]

Why does the test fail? Could it be that JUnit checks if the exception is thrown before Spring commits the transaction?

like image 625
Wim Deblauwe Avatar asked May 24 '16 07:05

Wim Deblauwe


People also ask

Does @transactional throw exception?

@Transactional only rolls back transactions for unchecked exceptions. For checked exceptions and their subclasses, it commits data. So although an exception is raised here, because it's a checked exception, Spring ignores it and commits the data to the database, making the system inconsistent.

How do you handle exception with transactional in spring boot?

In its default configuration, the Spring Framework's transaction infrastructure code marks a transaction for rollback only in the case of runtime, unchecked exceptions. That is, when the thrown exception is an instance or subclass of RuntimeException. (Error instances also, by default, result in a rollback).

When and where do you use @transactional in testing?

@Transactional spans the transaction for entire test method, so if you use some dao (like in your case) that transaction will be rolledback also, however if some method uses transaction propagation type other than REQUIRED , for example REQUIRED_NEW , call to db can be performed anyway, because REQUIRED_NEW suspends ...

Which annotation can be used along with the Transactional annotation to Override the transaction configuration?

With either frameworks (or rather: all frameworks in the Spring ecosystem), you will always use the @Transactional annotation, combined with a transaction manager and the @EnableTransactionManagement annotation. There is no other way.


2 Answers

You don't need to commit. You just need to flush your unit of work to the database in order to see the DataIntegrityViolationException.

This is described in the note on "false positives" in the Spring Reference Manual. Note, however, that the EntityManager must be injected via @PersistenceContext (not @Autowired).

The following should work fine for you:

@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
public class UserRepositoryIntegrationTest {

    @Autowired
    UserRepository repository;

    @PersistenceContext
    EntityManager entityManager;

    @Test(expected = DataIntegrityViolationException.class)
    public void test() {
        repository.save(User.createAdmin(repository.nextId(), "wim", "123456"));
        repository.save(User.createAdmin(repository.nextId(), "wim", "123456"));

        entityManager.flush();
    }
}

Regards,

Sam (author of the Spring TestContext Framework)

like image 187
Sam Brannen Avatar answered Sep 23 '22 20:09

Sam Brannen


Based on the comments by M.Deinum and Stephane Nicoll, this is the working version:

@SpringBootTest
@RunWith(SpringRunner.class)
@Transactional
public class UserRepositoryIntegrationTest {

    @Autowired
    private UserRepository repository;

    @Test(expected = DataIntegrityViolationException.class)
    public void test() {
        repository.save(User.createAdmin(repository.nextId(), "wim", "123456"));
        repository.save(User.createAdmin(repository.nextId(), "wim", "123456"));

        TestTransaction.flagForCommit();
        TestTransaction.end();
    }
}

Note that both static methods need to be called to make it work.

like image 44
Wim Deblauwe Avatar answered Sep 22 '22 20:09

Wim Deblauwe