Search code examples
mongodbspring-data-mongodbtestcontainersspring-boot-testcontainers

@Transactional in Spring Data MongoDB (+testcontainers)


I am failing to get the transactions to work in my basic spring data mongodb app. I've read articles and similar questions here and nothing seems to help. The test is being run with Testcontainers, which appears to launch Mongo with a replica set, meaning transactions should normally work. I am sure that the test config works fine at least with non-transactional code (more in the github link at the end).

The domain service:

public class EntityDomainService {

  private final EntityARepository repository;
  private final EntityBRepository entityBRepository;

  EntityDomainService(EntityARepository repository, EntityBRepository entityBRepository) {
    this.repository = repository;
    this.entityBRepository = entityBRepository;
  }

  @Transactional
  public void transactionalAdd(String id1, String id2) {
    repository.save(EntityA.builder().id(id1).build());
    fail();
    repository.save(EntityA.builder().id(id2).build());
  }

  private void fail() {
    throw new RuntimeException();
  }
}

Module-specific configuration:

@Configuration
@EnableMongoRepositories
class EntityDomainConfiguration {
  @Bean
  EntityDomainService domainService(
      EntityARepository entityARepository, EntityBRepository entityBRepository) {
    return new EntityDomainService(entityARepository, entityBRepository);
  }
}

"Global" Mongo config:

@Configuration
@EnableMongoRepositories()
@EnableTransactionManagement
class ApplicationConfiguration extends AbstractMongoClientConfiguration {
  // usual stuff

  @Bean
  MongoTransactionManager transactionManager(MongoDatabaseFactory dbFactory) {
    return new MongoTransactionManager(dbFactory);
  }
}

Test Mongo container config:

@EnableMongoRepositories
public class MongoDBContainerTestConfiguration {

  @Bean
  @ServiceConnection
  public MongoDBContainer mongoDBContainer() {
    return new MongoDBContainer(DockerImageName.parse("mongo:7.0")).withExposedPorts(27017);
  }
}

And finally, the test itself:

@Testcontainers
@DataMongoTest
@ContextConfiguration(classes = [MongoDBContainerTestConfiguration.class])
class EntityADomainSpec extends Specification {

    @Autowired EntityAMongoRepository simulationRepository
    @Autowired EntityBMongoRepository plotRepository

    EntityDomainService service

    def setup() {
        service = new EntityDomainConfiguration().domainService(simulationRepository, plotRepository)
    }

    def cleanup() {
        simulationRepository.deleteAll()
        plotRepository.deleteAll()
    }

    def "basic transaction test"() {
        when:
        service.transactionalAdd("abc", "def")

        then:
        thrown(RuntimeException)
        !simulationRepository.existsById("abc")
    }
}

Entire minimal reproducible example is hosted on my github (I hope it's pruned to the minimum, but that's my first shot at this). It contains one passing test (to show validity of the config) and the actual test of the transactional behavior: https://github.com/rafalszulejko/mongotransactions

EDIT: removed a lot of code from the github-hosted example


Solution

  • The answer was add the non-test configuration into @ContextConfiguration and autowire the service into the test class.

    @ContextConfiguration(classes = [EntityDomainConfiguration.class, MongoDBContainerTestConfiguration.class])
    class EntityADomainSpec extends Specification {
    
        @Autowired EntityAMongoRepository entityARepository
        @Autowired EntityDomainService service
    

    The consequence is that I cannot mock any dependecies of such service, but I guess that's whay you have to give away.