Test Fails with DataJpaTest Annotation

I am working on writing some tests for an entity class to make sure that the column restrictions I’ve placed on the properties of that entity are enforced. This is to make sure that these restrictions are only change intentionally.

Below is the test thus far:

@DataJpaTest
@ActiveProfiles("test")
class AccountLookupEntityIntegrationTest {

    @Autowired
    private TestEntityManager testEntityManager;

    private AccountLookupEntity accountLookupEntity;

    private static final String TEST_ACCOUNT_ID = "test-account-id";
    private static final String TEST_EMAIL = "test@test.com";
    @BeforeEach
    void setup() {
        accountLookupEntity = new AccountLookupEntity();
        accountLookupEntity.setAccountId(TEST_ACCOUNT_ID);
        accountLookupEntity.setEmail(TEST_EMAIL);
    }

    @Test
    void testAccountLookupEntity_whenValidAccountDetailsProvided_shouldReturnStoredAccountDetails() {
        // Act
        AccountLookupEntity storedAccountLookupEntity = testEntityManager.persistAndFlush(accountLookupEntity);

        // Assert
        assertEquals(accountLookupEntity.getAccountId(), storedAccountLookupEntity.getAccountId(), "AccountIds should match");
        assertEquals(accountLookupEntity.getEmail(), storedAccountLookupEntity.getEmail(), "Emails should match");
    }
}

And here is the application-test.yml file that holds the config for the h2 database being used for the test:

spring:
  datasource:
    url: jdbc:h2:mem:testdb
    driver-class-name: org.h2.Driver
    username: sa
    password: password
  jpa:
    database-platform: org.hibernate.dialect.H2Dialect

If necessary you can see all the code for the repository here

So the root of my issue I believe is summarized in this log statement:

{"timestamp":"2024-10-22-T23:25:00,668Z","level":"ERROR","logger":"org.springframework.boot.diagnostics.LoggingFailureAnalysisReporter","message":"\n\n***************************\nAPPLICATION FAILED TO START\n***************************\n\nDescription:\n\nParameter 1 of method registerAccountCommandInterceptors in loyalty.service.LoyaltyCommandApiApplication required a bean of type 'org.axonframework.commandhandling.CommandBus' that could not be found.\n\n\nAction:\n\nConsider defining a bean of type 'org.axonframework.commandhandling.CommandBus' in your configuration.\n"}

Which to me points to the interceptor setups that I do in my spring application class:

@EnableDiscoveryClient
@EnableScheduling
@SpringBootApplication
public class LoyaltyCommandApiApplication {

	public static void main(String[] args) {
		SpringApplication.run(LoyaltyCommandApiApplication.class, args);
	}

	@Autowired
	public void registerAccountCommandInterceptors(ApplicationContext context, CommandBus commandBus) {
		commandBus.registerDispatchInterceptor(
				context.getBean(ValidateCommandInterceptor.class)
		);
		commandBus.registerDispatchInterceptor(
				context.getBean(AccountCommandsInterceptor.class)
		);
		commandBus.registerDispatchInterceptor(
				context.getBean(BusinessCommandsInterceptor.class)
		);
		commandBus.registerDispatchInterceptor(
				context.getBean(LoyaltyBankCommandsInterceptor.class)
		);
		commandBus.registerDispatchInterceptor(
				context.getBean(TransactionCommandsInterceptor.class)
		);
	}

	@Autowired
	public void configure(EventProcessingConfigurer configurer) {
		// TODO: Save group strings to constants
		configurer.registerListenerInvocationErrorHandler("account-lookup-group",
				configuration -> new LoyaltyServiceEventsErrorHandler());
		configurer.registerListenerInvocationErrorHandler("business-lookup-group",
				configuration -> new LoyaltyServiceEventsErrorHandler());
		configurer.registerListenerInvocationErrorHandler("loyalty-bank-lookup-group",
				configuration -> new LoyaltyServiceEventsErrorHandler());
		configurer.registerListenerInvocationErrorHandler("redemption-tracker-group",
				configuration -> new LoyaltyServiceEventsErrorHandler());
		configurer.registerListenerInvocationErrorHandler("expiration-tracker-group",
				configuration -> new LoyaltyServiceEventsErrorHandler());

	}
}

But I am not entirely sure that this is correct. I understand that this is somewhat of a vague question and maybe this is more of a spring issue than it is an Axon related one, but I am currently at a loss for trying to determine why the @DataJpaTest annotation doesn’t work. If I use @SpringTest then the test will sort of run, but the TestEntityManager doesn’t work and ideally I would like to keep the application context of these tests as small as possible.

If anyone has any suggestions as to how to properly configure my environment to be able to use the @DataJpaTest annotation for testing these entities it would be greatly appreciated.

And duck debugging strikes again!

After writing out the question for this post, I thought about the issue being related to my interceptor and event handler initialization directly in my spring application class. This lead me to the idea of moving those initializer methods to a separate configuration class.

@Configuration
public class AxonConfig {

    @Autowired
    public void registerAccountCommandInterceptors(ApplicationContext context, CommandBus commandBus) {
        commandBus.registerDispatchInterceptor(
                context.getBean(ValidateCommandInterceptor.class)
        );
        commandBus.registerDispatchInterceptor(
                context.getBean(AccountCommandsInterceptor.class)
        );
        commandBus.registerDispatchInterceptor(
                context.getBean(BusinessCommandsInterceptor.class)
        );
        commandBus.registerDispatchInterceptor(
                context.getBean(LoyaltyBankCommandsInterceptor.class)
        );
        commandBus.registerDispatchInterceptor(
                context.getBean(TransactionCommandsInterceptor.class)
        );
    }

    @Autowired
    public void configure(EventProcessingConfigurer configurer) {
        // TODO: Save group strings to constants
        configurer.registerListenerInvocationErrorHandler("account-lookup-group",
                configuration -> new LoyaltyServiceEventsErrorHandler());
        configurer.registerListenerInvocationErrorHandler("business-lookup-group",
                configuration -> new LoyaltyServiceEventsErrorHandler());
        configurer.registerListenerInvocationErrorHandler("loyalty-bank-lookup-group",
                configuration -> new LoyaltyServiceEventsErrorHandler());
        configurer.registerListenerInvocationErrorHandler("redemption-tracker-group",
                configuration -> new LoyaltyServiceEventsErrorHandler());
        configurer.registerListenerInvocationErrorHandler("expiration-tracker-group",
                configuration -> new LoyaltyServiceEventsErrorHandler());

    }
}

Then I set the test using @DataJpaTest annotation to exclude the configuration class with the following annotation applied to the test class:

@ImportAutoConfiguration(exclude = {AxonConfig.class})

And now I am able to use the @DataJpaTest annotation with the TestEntityManager.

I hope this helps anyone else who may encounter this issue in the future.

2 Likes

Always great to find a post that’s answered by the post-owner. Great work, @ajisrael, and amazing that you’re sharing your solution with all of us :pray:

However, I have a hint given the sample you’ve provided, specifically concerning @Autowired annotated configuration methods. Although they work frequently, I have seen Spring ordering f* it up from time to time, sadly. What’s worked for me consistently instead, is providing a ConfigurerModule bean to the Application Context.

The ConfigurerModule is a functional interface provided by Axon Framework that, which is essentially a Supplier<Configurer>. Furthermore, Axon’s Spring Boot setup will look for all ConfigurerModule beans and attach them to the Configurer to ensure they’re invoked.

Hence, the ConfigurerModule allows a means to register beans that add configuration blocks to Axon Framework, instead of relying on sometimes flaky Spring ordering when it comes to using the @Autowired annotation.

Looking at your sample, you could thus rewrite it like so:

@Configuration
public class AxonConfig {

    @Bean
    public ConfigurerModule commandInterceptorConfigurerModule(ApplicationContext context) {
        return configurer -> configurer.onInitialize(config -> {
            CommandBus commandBus = config.commandBus();
            commandBus.registerDispatchInterceptor(context.getBean(ValidateCommandInterceptor.class));
            commandBus.registerDispatchInterceptor(context.getBean(AccountCommandsInterceptor.class)));
            commandBus.registerDispatchInterceptor(context.getBean(BusinessCommandsInterceptor.class)));
            commandBus.registerDispatchInterceptor(context.getBean(LoyaltyBankCommandsInterceptor.class)));
            commandBus.registerDispatchInterceptor(context.getBean(TransactionCommandsInterceptor.class)));
        });
    }

    @Bean
    public ConfigurerModule eventProcessorErrorHandlerConfigurerModule() {
        return configurer -> configurer.eventProcessing()
                                       .registerListenerInvocationErrorHandler(
                                               "account-lookup-group",
                                               config -> new LoyaltyServiceEventsErrorHandler()
                                       )
                                       .registerListenerInvocationErrorHandler(
                                               "business-lookup-group",
                                               config -> new LoyaltyServiceEventsErrorHandler()
                                       )
                                       .registerListenerInvocationErrorHandler(
                                               "loyalty-bank-lookup-group",
                                               config -> new LoyaltyServiceEventsErrorHandler()
                                       )
                                       .registerListenerInvocationErrorHandler(
                                               "redemption-tracker-group",
                                               config -> new LoyaltyServiceEventsErrorHandler()
                                       )
                                       .registerListenerInvocationErrorHandler(
                                               "expiration-tracker-group",
                                               config -> new LoyaltyServiceEventsErrorHandler()
                                       );
    }
}

Although you already solved your problem, I figured it might be helpful to see this approach as well.

1 Like

Thank you for the suggestion. When I initially used your implementation, I ran into a circular dependency issue within Spring. I think it was primarily related to the CommandGateway being Autowired into some of my other classes. If I add the @Lazy annotation to the CommandGateway properties of my PointExpirationService and AccountCommandController classes then the application is able to start.

To me adding in this annotation sporadically indicates I’m missing something. As I believe the main reason for this change is to have more granular control over how the application context loads.

That being said, you can see the changes in my PR (I hope). I was wondering if you could either clarify, or help me understand why it is only in these two places that the @Lazy annotation is required and if there is some additional configuration that I should consider?

If this isn’t really the place for this discussion, please don’t feel like this needs to be answered, as I know we’re getting a little off the topic and this is something very specific to my implementation at this point.

To be honest with you, I have never had to use the @Lazy annotation for this :person_shrugging:
Maybe it helps if you only use constructor-based wiring for your services? Sadly, Spring feels a little finicky to me from time to time. I wish I could help you better on this, @ajisrael, but this would require some deep diving I don’t have time for, I am afraid.

1 Like

I completely understand. Thank you so much for the help and suggestions!

1 Like