javajpatransactionsjunit5rollback

@Rollback @Transactional is not working in acceptance test


I saw similar example in Stackoverflow and elsewhere, but it is not working for me. I use spring boot data jpa, junit5, h2 database and trying to make a simple acceptance test. Tests OK, but I was want that the changes in the database will be reverted after the test, but they are staying committed instead. I the tests I create a row in database and in other test I delete a row. Here is the layers of my code: The test:

@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)

public class ControllerAcceptanceTest {

@LocalServerPort
int randomServerPort;
private RestTemplate restTemplate;
private String url;

@BeforeEach
void setUp() {
    restTemplate = new RestTemplate();
}

@Test
@Transactional
@Rollback
void testCreate() throws Exception {
    url = "http://localhost:" + randomServerPort + "/api/wallet/create";
    UUID uuid = UUID.randomUUID();
    PlayerDto playerDto = new PlayerDto();
    playerDto.setPlayerId(String.valueOf(uuid));
    ResponseEntity responseEntity = restTemplate.postForEntity(url, playerDto, Object.class);
    assertEquals(HttpStatus.OK, responseEntity.getStatusCode());
}

@Test
@Transactional
@Rollback
void testDelete() throws Exception {
    url = "http://localhost:" + randomServerPort + "/api/wallet/delete";
    WalletDto walletDto = new WalletDto();
    walletDto.setWalletId("1");
    ResponseEntity responseEntity = restTemplate.postForEntity(url, walletDto, Object.class);
    assertEquals(HttpStatus.OK, responseEntity.getStatusCode());
}

}

Controller:

@PostMapping (path = "/create", consumes = MediaType.APPLICATION_JSON_VALUE)
public void createWallet(@RequestBody PlayerDto playerDto) throws PlayerIdRedundantException {
    walletService.createWallet(playerDto.getPlayerId());
}
@PostMapping (path = "/delete", consumes = MediaType.APPLICATION_JSON_VALUE)
public void deleteWallet(@RequestBody WalletDto walletDto) throws PlayerIdRedundantException {
    walletService.deleteWallet(walletDto.getWalletId());
}

Service:

@Transactional
public void createWallet(String playerId) throws PlayerIdRedundantException {
    checkPlayerId(playerId);
    Wallet wallet = Wallet.builder()
            .playerId(playerId)
            .balance(BigDecimal.valueOf(0))
            .build();
    walletRepository.save(wallet);
}
@Transactional
public void deleteWallet(String walletId) throws PlayerIdRedundantException {

    Wallet wallet = walletRepository.findById(Long.valueOf(walletId)).orElseThrow();
    walletRepository.delete(wallet);
}

application.properties:

spring.h2.console.enabled=true
spring.datasource.url=jdbc:h2:file:./src/main/resources/data/testdb;AUTO_SERVER=TRUE;OLD_INFORMATION_SCHEMA=TRUE
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.generate-ddl=true

Solution

  • That's a common pitfall for transaction management when testing your Spring Boot application.

    You have to differentiate between a test-managed and a spring-managed transaction. When using @Transactional on a method/class in your test, Spring Test will wrap the test execution within a transaction and will by default roll back any change to the database (so @Rollback is redundant).

    This only applies to changes to the database that you directly make within this test-managed transaction, so if you would call myRepository.save() within the test method directly.

    In your example, you're writing an acceptance test that hits your application from the outside with HTTP, like your clients/frontend would during production. This will trigger a Spring-managed transaction when your call reaches your service and commit the changes once the call finishes.

    So you're actually having two transactions: the test-managed during your test execution and the Spring-managed inside your started application. From your test, you don't have access to the Spring-managed transaction and your rollback approach won't have a handle on this transaction inside the real application context.

    To fix, you can, e.g., call repository.deleteAll() in a BeforeEach or AfterEach.

    Also, consult the Javadoc of the TransactionalTestExecutionListener for a more detailed explanation.