79304539

Date: 2024-12-24 01:05:05
Score: 0.5
Natty:
Report link

As mentioned above, in order for my service under test to be correctly wired up to be Proxied, it needs to be annotated in the test as @Autowired.

And, in order to make my service @Autowired while still being able to mock a failure, I needed to change my dependencies from being @Autowired to using @SpyBean.

In addition, @MybatisTest is itself @Transactional, so that masks whether the rollback in my application code is actually working. So, I have fallen back to using @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()); 
        });
    }
}

Finally, one other tweak I had to do was due to MyService having an @Autowired constructor (vs autowired fields). This seemed to cause problems for the @SpyBeans. The fix for this was to mark the constructor parameters as @Lazy.

With all these changes, I was able to use @Transactional instead of the transaction manager approach.

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

Hope this is helpful for someone else.

Reasons:
  • Blacklisted phrase (0.5): I need
  • Long answer (-1):
  • Has code block (-0.5):
  • Self-answer (0.5):
  • Low reputation (1):
Posted by: Sydney