在 spring-boot 集成测试中验证事务回滚

问题描述 投票:0回答:1

我正在尝试编写一个集成测试,以验证如果事务中的某个操作失败,数据库行是否会回滚。但是,虽然我看到回滚被触发,但我的数据库更改实际上并未回滚。

我选择使用 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()); 
        });
    }
}

为什么我没有从数据库角度看到实际的回滚?

提前致谢!

spring-boot mybatis spring-transactions
1个回答
0
投票

如上所述,为了使我的被测服务能够正确连接到代理,需要在测试中将其注释为

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

希望这对其他人有帮助。

© www.soinside.com 2019 - 2024. All rights reserved.