spring-projects / spring-projects/spring-modulith
Adding `@ModuleSlicing` to a `@DataJpaTest` annotated test causes non persistence related beans to be included in the module slice
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 1.2k
- Forks
- 222
- PR merge metrics
- No merged PRs in 30d
Description
I have a working test for testing a JPA repository that is annotated with @DataJpaTest.
The test only instantiates beans of the persistence slice, no controllers, no services.
After adding @ModuleSlicing(extraIncludes = ["integrationtest"]) the test suddenly creates beans from the service and web layer such as @Controller or @Service annotated classes.
This is using latest modulith and boot RC
spring-boot = "4.1.0-RC1"
spring-modulith-bom = "2.1.0-RC1"
I have some simpler modules with less dependencies to other modules where @ModuleSlicing perfectly works, but it is not working with complex ones that have a larger depth of module dependencies.
I know, the following is quite a big mouthful, hard to digest. For now I'm only asking for support on how to further diagnose the parts of Spring Modulith that customize the @DataJpaTest's annotation effects for narrowing bean dependencies to only the tested module.
So far I have not been able to create an example that has the same issue, nor to find a proper way of debugging that.
Here is some of the involved code first, followed by some of the log output that gives overview of the modules found.
package at.customer.app.delivery.internal
@ModuleSlicing(extraIncludes = ["integrationtest"])
@DataJpaTest
@AutoConfigureTestDatabase(replace = Replace.NONE)
@TestPropertySource(properties = """
spring.jpa.show-sql=true
spring.modulith.runtime.flyway-enabled=true
""")
@Import([IntegrationTestConfiguration, DeliveryFactoryTestFixturesConfiguration])
class EnvelopeRepositorySpec extends AbstractAggregateRootRepositorySpecification<Envelope, EnvelopeIdentifier> {
@Autowired
EnvelopeFactory entityFactory
@Autowired
EnvelopeRepository repository
}
The exception below shows that for some reason a controller is instantiated even though a @DataJpaTest annotation is present.
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'envelopeController' defined in file [/Users/martinahrer/Development/wertekassa/wertekassa-backend/build/classes/java/main/at/dig/wertekassa/delivery/EnvelopeController.class]: Unsatisfied dependency expressed through constructor parameter 0: Error creating bean with name 'envelopeService' defined in file [/Users/martinahrer/Development/wertekassa/wertekassa-backend/build/classes/java/main/at/dig/wertekassa/delivery/EnvelopeService.class]: Unsatisfied dependency expressed through constructor parameter 2: No qualifying bean of type 'at.dig.wertekassa.order.OrderService' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:804)
at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:240)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1382)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1221)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:565)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:525)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:333)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:371)
The test is importing 2 configurations that DO NOT USE component scanning but provide configuration for test containers and some test object factories. These imports are not needed as they mimic some of the @ModuleSlicing configuration by only including the vertical configurations of required modules. Removing them wont make it work
package at.customer.app.integrationtest
@Configuration
@Import([DatabaseTestcontainersConfiguration, PersistenceConfiguration])
class IntegrationTestConfiguration {
@Bean
AbstractAssociationAdapter associationAdapter() {
new JpaAssociationAdapter()
}
}
package at.customer.app.persistence;
@Configuration
@EnableJpaAuditing(dateTimeProviderRef = "utcDateTimeProvider")
public class PersistenceConfiguration {
@Bean
public DateTimeProvider utcDateTimeProvider() {
return () -> Optional.of(OffsetDateTime.now(ZoneOffset.UTC));
}
}
package at.customer.app;
@Configuration(proxyBeanMethods = false)
public class DatabaseTestcontainersConfiguration {
@Bean
@ServiceConnection
PostgreSQLContainer postgresContainer() {
return new PostgreSQLContainer(DockerImageName.parse("postgres:latest"))
.withReuse(true)
.withUsername("spring")
.withPassword("boot")
.withDatabaseName("wertekassa");
}
}
DeliveryFactoryTestFixturesConfiguration.groovy provides factories for the module and loads another module's factory configuration
package at.customer.app.delivery
@Configuration
@Import(OrderFactoryTestFixturesConfiguration)
class DeliveryFactoryTestFixturesConfiguration {
@Bean
EnvelopeFactory envelopeFactory(OrderFactory orderFactory, EnvelopePositionFactory envelopePositionFactory, AbstractAssociationAdapter associationAdapter) {
new EnvelopeFactory(Optional.of(orderFactory), Optional.of(envelopePositionFactory), Optional.of(associationAdapter))
}
...
}
Module delivery classes
└── at
└── customer
└── app
├── delivery
│ ├── Address.java
│ ├── AddressFieldConstraint.java
│ ├── EmailAddress.java
│ ├── Envelope.java
│ ├── EnvelopeController.java <<<====
│ ├── EnvelopeIdentifier.java
│ ├── EnvelopeNumber.java
│ ├── EnvelopeNumberConstraint.java
│ ├── EnvelopePosition.java
│ ├── EnvelopePositionIdentifier.java
│ ├── EnvelopeService.java
│ ├── internal
│ │ ├── EnvelopeMapper.java
│ │ ├── EnvelopePositionMapper.java
│ │ ├── EnvelopePositionMapperDecorator.java
│ │ ├── EnvelopeRepository.java <<<====
│ │ ├── package-info.java
│ │ ├── StringToEnvelopeIdentifierConverter.java
│ │ ├── StringToTransportCarrierIdentifierConverter.java
│ │ ├── StringToTransportCompanyIdentifierConverter.java
│ │ ├── TransportCarrierMapper.java
│ │ ├── TransportCarrierRepository.java
│ │ ├── TransportCompanyMapper.java
│ │ └── TransportCompanyRepository.java
│ ├── InvalidArticleQuantities.java
│ ├── NameConstraint.java
│ ├── package-info.java
│ ├── TrackingNumber.java
│ ├── TrackingNumberConstraint.java
│ ├── TransportCarrier.java
│ ├── TransportCarrierController.java
│ ├── TransportCarrierIdentifier.java
│ ├── TransportCarrierService.java
│ ├── TransportCompany.java
│ ├── TransportCompanyController.java
│ ├── TransportCompanyIdentifier.java
│ ├── TransportCompanyService.java
│ └── workflow
│ ├── DeliveryWorkflowGuardFactory.java
│ └── package-info.java
integrationTest
└── at
└── customer
└── app
├── catalog
│ ├── internal
│ │ ├── ArticleRepositorySpec.groovy
│ │ ├── MarketPriceRepositorySpec.groovy
│ │ └── RicRateRepositorySpec.groovy
│ └── JMoleculesIdentifierIntegrationSpec.groovy
├── delivery
│ └── internal
│ ├── EnvelopeRepositorySpec.groovy <<<===
│ ├── TransportCarrierRepositorySpec.groovy
│ └── TransportCompanyRepositorySpec.groovy
├── integrationtest
│ ├── IntegrationTestConfiguration.groovy
│ ├── package-info.java
│ └── persistence
│ ├── AbstractAggregateRootRepositorySpecification.groovy
│ ├── AbstractRepositorySpecification.groovy
│ ├── JpaAssociationAdapter.groovy
│ └── package-info.java
├── inventory
│ └── internal
│ ├── InventoryRepositorySpec.groovy
│ └── TransactionRepositorySpec.groovy
├── notification
│ └── internal
│ ├── NotificationRepositorySpec.groovy
│ └── TaskRepositorySpec.groovy
├── order
│ ├── internal
│ │ ├── OrderArticlePriceSnapshotRepositorySpec.groovy
│ │ └── OrderRepositorySpec.groovy
│ └── OrderServiceSpec.groovy
├── orderworkflow
│ └── internal
│ ├── OrderWorkflowRepositorySpec.groovy
│ └── OrderWorkflowTransitionRepositorySpec.groovy
├── process
│ └── internal
│ └── ProcessTypeRepositorySpec.groovy
├── sample
│ └── SampleControllerSpec.groovy
└── security
├── AbstractKeycloakIntegrationSpec.groovy
├── KeycloakAdminServiceSpec.groovy
└── KeycloakIntegration.groovy
The log showing the modules as detected by Spring Modulith
ustomizerFactory$ModuleContextCustomizer : Bootstrapping @org.springframework.modulith.test.ApplicationModuleTest for Delivery in mode STANDALONE (class at.customer.app.ServerApplication)…
ustomizerFactory$ModuleContextCustomizer :
ustomizerFactory$ModuleContextCustomizer : # Delivery
ustomizerFactory$ModuleContextCustomizer : > Logical name: delivery
ustomizerFactory$ModuleContextCustomizer : > Base package: at.customer.app.delivery
ustomizerFactory$ModuleContextCustomizer : > Excluded packages: none
ustomizerFactory$ModuleContextCustomizer : > Named interfaces:
ustomizerFactory$ModuleContextCustomizer : + NamedInterface: name=<<UNNAMED>>, types=[ a.d.w.d.Address, a.d.w.d.AddressFieldConstraint, a.d.w.d.DeliveryFactoryTestFixturesConfiguration, a.d.w.d.EmailAddress, a.d.w.d.Envelope, a.d.w.d.EnvelopeController, a.d.w.d.EnvelopeFactory, a.d.w.d.EnvelopeFactory._closure1, a.d.w.d.EnvelopeFactory._closure2, a.d.w.d.EnvelopeFactory._closure3, a.d.w.d.EnvelopeFactory._postProcessChildren_closure4, a.d.w.d.EnvelopeIdentifier, a.d.w.d.EnvelopeNumber, a.d.w.d.EnvelopeNumberConstraint, a.d.w.d.EnvelopePosition, a.d.w.d.EnvelopePositionFactory, a.d.w.d.EnvelopePositionFactory._closure1, a.d.w.d.EnvelopePositionFactory._closure2, a.d.w.d.EnvelopePositionIdentifier, a.d.w.d.EnvelopeService, a.d.w.d.InvalidArticleQuantities, a.d.w.d.NameConstraint, a.d.w.d.TrackingNumber, a.d.w.d.TrackingNumberConstraint, a.d.w.d.TransportCarrier, a.d.w.d.TransportCarrierController, a.d.w.d.TransportCarrierFactory, a.d.w.d.TransportCarrierFactory._closure1, a.d.w.d.TransportCarrierFactory._closure2, a.d.w.d.TransportCarrierIdentifier, a.d.w.d.TransportCarrierService, a.d.w.d.TransportCompany, a.d.w.d.TransportCompanyController, a.d.w.d.TransportCompanyFactory, a.d.w.d.TransportCompanyFactory._closure1, a.d.w.d.TransportCompanyIdentifier, a.d.w.d.TransportCompanyService ]
ustomizerFactory$ModuleContextCustomizer : + NamedInterface: name=workflow, types=[ a.d.w.d.w.DeliveryWorkflowGuardFactory ]
ustomizerFactory$ModuleContextCustomizer : > Direct module dependencies:
ustomizerFactory$ModuleContextCustomizer : - catalog
ustomizerFactory$ModuleContextCustomizer : - integrationtest
ustomizerFactory$ModuleContextCustomizer : + order
ustomizerFactory$ModuleContextCustomizer : - persistence
ustomizerFactory$ModuleContextCustomizer : - service
ustomizerFactory$ModuleContextCustomizer : - statemachine
ustomizerFactory$ModuleContextCustomizer : + statespi
ustomizerFactory$ModuleContextCustomizer : - test
ustomizerFactory$ModuleContextCustomizer : - web
ustomizerFactory$ModuleContextCustomizer : > Spring beans:
ustomizerFactory$ModuleContextCustomizer : + ….DeliveryFactoryTestFixturesConfiguration
ustomizerFactory$ModuleContextCustomizer : + ….EnvelopeController
ustomizerFactory$ModuleContextCustomizer : + ….EnvelopeFactory
ustomizerFactory$ModuleContextCustomizer : + ….EnvelopePositionFactory
ustomizerFactory$ModuleContextCustomizer : + ….EnvelopeService
ustomizerFactory$ModuleContextCustomizer : + ….TransportCarrierController
ustomizerFactory$ModuleContextCustomizer : + ….TransportCarrierFactory
ustomizerFactory$ModuleContextCustomizer : + ….TransportCarrierService
ustomizerFactory$ModuleContextCustomizer : + ….TransportCompanyController
ustomizerFactory$ModuleContextCustomizer : + ….TransportCompanyFactory
ustomizerFactory$ModuleContextCustomizer : + ….TransportCompanyService
ustomizerFactory$ModuleContextCustomizer : o ….internal.EnvelopeMapperImpl
ustomizerFactory$ModuleContextCustomizer : o ….internal.EnvelopePositionMapperImpl
ustomizerFactory$ModuleContextCustomizer : o ….internal.EnvelopePositionMapperImpl_
ustomizerFactory$ModuleContextCustomizer : o ….internal.EnvelopeRepository
ustomizerFactory$ModuleContextCustomizer : o ….internal.StringToEnvelopeIdentifierConverter
ustomizerFactory$ModuleContextCustomizer : o ….internal.StringToTransportCarrierIdentifierConverter
ustomizerFactory$ModuleContextCustomizer : o ….internal.StringToTransportCompanyIdentifierConverter
ustomizerFactory$ModuleContextCustomizer : o ….internal.TransportCarrierMapperImpl
ustomizerFactory$ModuleContextCustomizer : o ….internal.TransportCarrierRepository
ustomizerFactory$ModuleContextCustomizer : o ….internal.TransportCompanyMapperImpl
ustomizerFactory$ModuleContextCustomizer : o ….internal.TransportCompanyRepository
ustomizerFactory$ModuleContextCustomizer : + ….workflow.DeliveryWorkflowGuardFactory
ustomizerFactory$ModuleContextCustomizer :
ustomizerFactory$ModuleContextCustomizer : Extra includes:
ustomizerFactory$ModuleContextCustomizer : > integrationtest
ustomizerFactory$ModuleContextCustomizer :
ustomizerFactory$ModuleContextCustomizer : Shared modules:
ustomizerFactory$ModuleContextCustomizer : > web, persistence, service
ustomizerFactory$ModuleContextCustomizer :
ustomizerFactory$ModuleContextCustomizer : Included dependencies:
ustomizerFactory$ModuleContextCustomizer :
ustomizerFactory$ModuleContextCustomizer : # Integration Test Support
ustomizerFactory$ModuleContextCustomizer : > Logical name: integrationtest
ustomizerFactory$ModuleContextCustomizer : > Base package: at.customer.app.integrationtest
ustomizerFactory$ModuleContextCustomizer : > Excluded packages: none
ustomizerFactory$ModuleContextCustomizer : > Named interfaces:
ustomizerFactory$ModuleContextCustomizer : + NamedInterface: name=<<UNNAMED>>, types=[ a.d.w.i.IntegrationTestConfiguration ]
ustomizerFactory$ModuleContextCustomizer : + NamedInterface: name=persistence, types=[ a.d.w.i.p.AbstractAggregateRootRepositorySpecification, a.d.w.i.p.AbstractRepositorySpecification, a.d.w.i.p.AbstractRepositorySpecification.__spock_initializeFields_closure1, a.d.w.i.p.JpaAssociationAdapter ]
ustomizerFactory$ModuleContextCustomizer : > Direct module dependencies:
ustomizerFactory$ModuleContextCustomizer : - persistence
ustomizerFactory$ModuleContextCustomizer : - test
ustomizerFactory$ModuleContextCustomizer : > Spring beans:
ustomizerFactory$ModuleContextCustomizer : + ….IntegrationTestConfiguration
ustomizerFactory$ModuleContextCustomizer : + ….persistence.JpaAssociationAdapter
ustomizerFactory$ModuleContextCustomizer : o at.customer.app.test.AbstractAssociationAdapter
ustomizerFactory$ModuleContextCustomizer : o at.customer.app.test.AssociationDetector
ustomizerFactory$ModuleContextCustomizer :
ustomizerFactory$ModuleContextCustomizer : # Common Web Support
ustomizerFactory$ModuleContextCustomizer : > Logical name: web
ustomizerFactory$ModuleContextCustomizer : > Base package: at.customer.app.web
ustomizerFactory$ModuleContextCustomizer : > Excluded packages: none
ustomizerFactory$ModuleContextCustomizer : > Direct module dependencies:
ustomizerFactory$ModuleContextCustomizer : - service
ustomizerFactory$ModuleContextCustomizer : > Spring beans:
ustomizerFactory$ModuleContextCustomizer : + ….DefaultControllerAdvice
ustomizerFactory$ModuleContextCustomizer : o ….internal.JsonApiJMoleculesJacksonConfiguration
ustomizerFactory$ModuleContextCustomizer : o ….internal.JsonMapperConfiguration
ustomizerFactory$ModuleContextCustomizer : o com.toedter.spring.hateoas.jsonapi.JsonApiConfiguration
ustomizerFactory$ModuleContextCustomizer : o org.springframework.boot.jackson.autoconfigure.JsonMapperBuilderCustomizer
ustomizerFactory$ModuleContextCustomizer :
ustomizerFactory$ModuleContextCustomizer : # Common Persistence Support
ustomizerFactory$ModuleContextCustomizer : > Logical name: persistence
ustomizerFactory$ModuleContextCustomizer : > Base package: at.customer.app.persistence
ustomizerFactory$ModuleContextCustomizer : > Excluded packages: none
ustomizerFactory$ModuleContextCustomizer : > Direct module dependencies: none
ustomizerFactory$ModuleContextCustomizer : > Spring beans:
ustomizerFactory$ModuleContextCustomizer : + ….PersistenceConfiguration
ustomizerFactory$ModuleContextCustomizer : o ….internal.FlywayConfiguration
ustomizerFactory$ModuleContextCustomizer : o org.springframework.boot.flyway.autoconfigure.FlywayMigrationStrategy
ustomizerFactory$ModuleContextCustomizer : o org.springframework.data.auditing.DateTimeProvider
ustomizerFactory$ModuleContextCustomizer :
ustomizerFactory$ModuleContextCustomizer : # Common Service Support
ustomizerFactory$ModuleContextCustomizer : > Logical name: service
ustomizerFactory$ModuleContextCustomizer : > Base package: at.customer.app.service
ustomizerFactory$ModuleContextCustomizer : > Excluded packages: none
ustomizerFactory$ModuleContextCustomizer : > Direct module dependencies:
ustomizerFactory$ModuleContextCustomizer : - persistence
ustomizerFactory$ModuleContextCustomizer : > Spring beans: none
ustomizerFactory$ModuleContextCustomizer :
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with integrationTest/at/customer/app/delivery/internal/EnvelopeRepositorySpec.groovy and compare the effects of @ModuleSlicing and @DataJpaTest with the imported IntegrationTestConfiguration and DeliveryFactoryTestFixturesConfiguration. Inspect the module-slicing test customization and the logged Delivery module contents to determine why EnvelopeController and other service-layer beans are included; done means reproducing the unwanted bean creation and preventing it without breaking the repository slice.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- groovy, java, spring, spring-boot
- Domain
- backend, testing-qa
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Needs clarification
- Newbie friendliness
- 45/100