mmjmanders
mmjmanders

Reputation: 1553

Transactions in spring boot testing not rolled back

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);
}

Upvotes: 17

Views: 11568

Answers (1)

Alex Saunin
Alex Saunin

Reputation: 1015

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 either RANDOM_PORT or DEFINED_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.

Upvotes: 38

Related Questions