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?
@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.
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).
@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 ...
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.
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)
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With