I am trying to write an integration test that verifies database rows are rolled back if one of the operations in a transaction fails. However, though I see the rollback being triggered, my database changes are not actually rolled back.
I’ve opted to go with a PlatformTransactionManager approach to avoid all the magic of @Transaction (at least for now) so that I can see what’s going on.
My service with the transactional method looks like:
@Service
public class ThingService {
private final ThingDao thingDao;
private final PlatformTransactionManager txManager;
@Autowired
public ThingService(final ThingDao dao, final PlatformTransactionManager txManager) {
this.thingDao = dao;
this.txManager = txManager;
}
public void createOrUpdate(final List<ThingRequest> requests) {
final TransactionDefinition txDef = new DefaultTransactionDefinition();
final TransactionStatus txStatus = txManager.getTransaction(txDef);
try {
requests.forEach(c -> thingDao.createThing(ThingModel.fromCreateRequest(c)));
txManager.commit(txStatus);
} catch (final Exception e) {
txManager.rollback(txStatus);
throw e;
}
}
public void createUpdateThings(final List<ThingRequest> requests) throws ControllerBadRequestException {
try {
createOrUpdate(requests);
} catch (final Throwable t) {
logger.error("A database exception occurred during createUpdateThings", t);
throw new MyBadRequestException(t);
}
}
}
My test is set up so that 3 rows should be inserted and then the final insert should trigger a RuntimeException. In my test, I see that the rollback is supposed to have occurred (i.e., the assertThrows
line passes, and I can debug and see that the txManager.rollback
line is hit. However, when I make calls to the database to verify the first 3 rows are rolled back, it seems they are not.
My TransactionManager appears to be correctly initialized (in that I can see it is not null and has the expected properties).
Here is the test code:
@MybatisTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class ThingServiceIntegrationTest extends AbstractPostgresJupiterTest {
// NOTE: AbstractPostgresJupiterTest sets up a PostgreSQLContainer and initializes datasource properties
// Works find for all other integration tests except this transactional test
@Autowired
private ThingMapper mapper;
@Autowired
private PlatformTransactionManager txManager;
private ThingService service;
private ThingDao dao;
private @NotNull ThingService makeTestService() {
class TestDao extends ThingDao {
public TestDao(final ThingMapper thingMapper) {
super(thingMapper);
}
@Override
public UUID createThing(final ThingModel model) {
if (Objects.equals(model.name(), "Bad Request")) {
throw new RuntimeException("Database exception");
}
return super.createThing(model);
}
}
final ThingDao daoWithFailure = new TestDao(mapper);
return new ThingService(daoWithFailure, txManager);
}
@Test
@Description("Should rollback on error")
public void rollbackOnError() throws MyBadRequestException {
final ThingService thingService = makeTestService();
final List<ThingRequest> createRequests = Stream.of(1, 3)
.map(i -> generateThingRequest())
.toList();
final ThingRequest badRequest = createRequests.getLast().withName("Bad Request");
final List<ThingRequest> all = new ArrayList<>(createRequests);
all.add(badRequest);
assertThrows(MyBadRequestException.class, () -> thingService.createUpdateThings(all));
// should rollback all inserts
createRequests.forEach(req -> {
// FAILS HERE
assertTrue(dao.getByName(req.name()).isEmpty());
});
}
}
Why am I not seeing the actual rollback from the db perspective?
Thanks in advance!
Sydney is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
2