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

Open
#1,681 1 comment 0 reactions 0 assignees View on GitHub

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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.