I have an integrations test class for my UserController
. The contents of the following class are:
// imports...
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@RunWith(SpringRunner.class)
@Transactional
@Rollback
public class UserControllerTests {
private static final String ENDPOINT = "/v1/users";
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private ApplicationProperties applicationProperties;
@Test
public void test_user_create() {
String token = login("test", "test");
HttpEntity<UserRequest> request = createRequest(token, "admin", "admin");
ResponseEntity<User> response = restTemplate.exchange(ENDPOINT, HttpMethod.POST, request, User.class);
assertEquals(HttpStatus.CREATED, response.getStatusCode());
}
private HttpEntity createRequest(String token) {
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
headers.set("Authorization", String.format("Bearer %s", token));
return new HttpEntity(headers);
}
private HttpEntity<UserRequest> createRequest(String token, String username, String password) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
headers.set("Authorization", String.format("Bearer %s", token));
return new HttpEntity<>(new UserRequest(username, password), headers);
}
private String login(String username, String password) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
headers.set("Authorization", String.format("Basic %s", Base64.getEncoder().encodeToString(String.format("%s:%s", applicationProperties.getAuth().getClientId(), applicationProperties.getAuth().getClientSecret()).getBytes())));
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("grant_type", "password");
body.add("username", username);
body.add("password", password);
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(body, headers);
ResponseEntity<OAuth2AccessToken> response = restTemplate.exchange("/oauth/token", HttpMethod.POST, request, OAuth2AccessToken.class);
return response.getBody().getValue();
}
}
When I execute this test class twice, the second time it fails because there is already a user in the database with username admin
(unique constraint).
I am testing against a postgres
database which is the same as in my production environment. The application is using Spring's jdbcTemplate
for database operations.
My logging produced the following logs:
2017-10-13 14:11:31.407 INFO [iam-service,,,] 63566 --- [ main] o.s.t.c.transaction.TransactionContext : Began transaction (1) for test context
...
2017-10-13 14:11:32.050 INFO [iam-service,,,] 63566 --- [ main] o.s.t.c.transaction.TransactionContext : Rolled back transaction for test context
My application flow is <request> --> <controller> --> <service with jdbcTemplate>
and the services are annotation with @Transactional
.
I'm really stuck with this.
One solution found didn't work for me, it was creating a PlatformTransactionManager
bean for the test configuration:
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
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 ...
@Rollback is a test annotation that is used to indicate whether a test-managed transaction should be rolled back after the test method has completed.
The @Transactional annotation makes use of the attributes rollbackFor or rollbackForClassName to rollback the transactions, and the attributes noRollbackFor or noRollbackForClassName to avoid rollback on listed exceptions. The default rollback behavior in the declarative approach will rollback on runtime exceptions.
According to the official Spring Boot documentation db transaction rollback is not supported when you apply it directly from the "web layer":
If your test is
@Transactional
, it will rollback the transaction at the end of each test method by default. However, as using this arrangement with eitherRANDOM_PORT
orDEFINED_PORT
implicitly provides a real servlet environment, HTTP client and server will run in separate threads, thus separate transactions. Any transaction initiated on the server won’t rollback in this case.
I propose you to consider the following options:
Use separate tests for web controller layer and database layer in case of Unit testing
Create/Restore tables before & Drop/Clear them after the test method execution when integration tests are performed. This approach might have significant overhead when the Db schema is large, but you can clear/restore data selectively according to you demands.
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