我正在尝试编写一个集成测试,以验证如果事务中的某个操作失败,数据库行是否会回滚。但是,虽然我看到回滚被触发,但我的数据库更改实际上并未回滚。
我选择使用 PlatformTransactionManager 方法来避免 @Transaction 的所有魔力(至少现在是这样),这样我就可以看到发生了什么。
我的事务方法服务如下所示:
@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);
}
}
}
我的测试设置为应插入 3 行,然后最终插入应触发 RuntimeException。在我的测试中,我看到回滚应该已经发生(即,
assertThrows
行通过,我可以调试并看到txManager.rollback
行被击中。但是,当我调用数据库来验证时前 3 行已回滚,似乎没有回滚。
我的 TransactionManager 似乎已正确初始化(因为我可以看到它不为空并且具有预期的属性)。
这是测试代码:
@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());
});
}
}
为什么我没有从数据库角度看到实际的回滚?
提前致谢!
如上所述,为了使我的被测服务能够正确连接到代理,需要在测试中将其注释为
@Autowired
。
并且,为了使我的服务
@Autowired
同时仍然能够模拟失败,我需要将我的依赖项从@Autowired
更改为使用@SpyBean
。
此外,
@MybatisTest
本身就是@Transactional
,因此掩盖了我的应用程序代码中的回滚是否真正起作用。 所以,我又开始使用@SpringBootTest
。
@SpringBootTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class ThingServiceIntegrationTest extends AbstractPostgresJupiterTest {
// NOTE: AbstractPostgresJupiterTest sets up a PostgreSQLContainer and initializes datasource properties
@SpyBean
private ThingMapper mapper;
@SpyBean
private ThingDao dao;
@Autowired
private ThingService service;
@Test
@Description("Should rollback on error")
public void rollbackOnError() {
final List<ThingRequest> createRequests = Stream.of(1, 3)
.map(i -> generateThingRequest())
.toList();
final ThingRequest badRequest = createRequests.getLast().withName("Bad Request");
final List<ThingRequest> all =
Stream.concat(createRequests.stream(), Stream.of(badRequest)).toList();
doThrow(new MyBadRequestException("bad")).when(dao)
.createThing(argThat(thing -> thing.id().equals(badCreate.id())));
assertThrows(MyBadRequestException.class, () -> thingService.createUpdateThings(all));
// should rollback all inserts
createRequests.forEach(req -> {
assertTrue(dao.getByName(req.name()).isEmpty());
});
}
}
最后,我必须做的另一项调整是由于
MyService
有一个 @Autowired
构造函数(相对于自动装配字段)。 这似乎给@SpyBeans
带来了问题。 解决此问题的方法是将构造函数参数标记为 @Lazy
。
通过所有这些更改,我能够使用
@Transactional
而不是事务管理器方法。
@Service
public class ThingService {
private final ThingDao thingDao;
@Autowired
public ThingService(@Lazy final ThingDao dao) {
this.thingDao = dao;
this.txManager = txManager;
}
@Transactional(rollbackFor = Throwable.class)
public void createOrUpdate(final List<ThingRequest> requests) {
requests.forEach(c -> thingDao.createThing(ThingModel.fromCreateRequest(c)));
}
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);
}
}
}
希望这对其他人有帮助。