spring state machine changing to one of two possible states based on action - spring-statemachine

i'm having trouble configuring my SSM for a very simple scenario:
the culprit is the 'validation' action that needs to happen when the log_on event is fired. i re-read the SSM documentation and am not sure if this is a 'fork', 'guard' or a hierarchical configuration.
the 'action' is performed here:
#Component
public class LogonValidationAction implements Action<PickStates, PickEvents> {
Logger logger = LogManager.getLogger();
volatile int counter = 0;
#Override
public void execute(StateContext<PickStates, PickEvents> context) {
String eventName = context.getEvent().name();
logger.info("executing {} for event {}", LogonValidationAction.class.getName(), eventName);
// basically, if success, send success, if failure, send failure
// for testing
if(counter % 2 == 0)
context.getStateMachine().sendEvent(PickEvents.logon_sucess);
else
context.getStateMachine().sendEvent(PickEvents.logon_fail);
}
}
i am using 'counter' in my test to direct the flow to either 'failure' (logged_off state remains) or 'success' (state changes to logged_on).
here's the configuration:
#Configuration
#EnableStateMachineFactory
public class Config extends EnumStateMachineConfigurerAdapter<PickStates, PickEvents>{
#Autowired
StateMachineListener stateMachineListener;
#Autowired
LogonValidationAction logonValidationAction;
#Override
public void configure(StateMachineStateConfigurer<PickStates, PickEvents> states) throws Exception {
states
.withStates()
.initial(PickStates.logged_off)
.state(PickStates.logged_on)
.states(EnumSet.allOf(PickStates.class));
}
#Override
public void configure(StateMachineTransitionConfigurer<PickStates, PickEvents> transitions) throws Exception {
transitions
.withExternal()
.source(PickStates.logged_off)
.target(PickStates.logged_on)
.event(PickEvents.log_on)
.action(logonValidationAction)
.and()
.withExternal()
.source(PickStates.logged_on)
.target(PickStates.logged_off)
.event(PickEvents.log_off);
}
#Override
public void configure(StateMachineConfigurationConfigurer<PickStates, PickEvents> config) throws Exception {
config
.withConfiguration()
.autoStartup(true)
.listener(stateMachineListener);
}
}
this is the failing test (expecting 'logged_off' when the action fires the logon_fail event):
#RunWith(SpringJUnit4ClassRunner.class)
#SpringApplicationConfiguration(classes = Application.class)
public class EzpickStateMachineTest {
Logger logger = LogManager.getLogger();
#Autowired
StateMachineFactory<PickStates, PickEvents> stateMachineFactory;
// for testing so can toggle success/failure when logging in
#Autowired
LogonValidationAction logonValidationAction;
#Test
public void failLogon() throws Exception {
StateMachine<PickStates, PickEvents> stateMachine = stateMachineFactory.getStateMachine();
stateMachine.start();
// when sm starts, state is 'logged_off'
assertThat(stateMachine.getState().getId().name(), is(PickStates.logged_off.name()));
// odd numbers fire 'failure' event
logonValidationAction.setCounter(1);
stateMachine.sendEvent(PickEvents.log_on);
// if logon fails, state is 'logged_off'
assertThat(stateMachine.getState().getId().name(), is(PickStates.logged_off.name()));
}
}

From action you try to send either logon_sucess or logon_fail but in machine configuration you haven't defined any transitions for those events so machine would not do anything.
Other notes, don't store counter in action bean, use machine extended state variables for it as then it becomes visible to every component using machine or StateContext.
When that counter is available in extended state, you can i.e. create an anonymous transition with a guard which uses that counter from extended state. One other possibility is to use choice pseudostate and guards to make a choice between target states. Your use of counter is exactly what wiki mention about in its extended state section https://en.wikipedia.org/wiki/UML_state_machine#Extended_states

Related

Tests fail with #Scheduled Task: JdbcSQLSyntaxErrorException Table "USER_ACCOUNT_CREATED_EVENT" not found

Summary & first problem
I am trying to test my user registration mechanism. When a new user account is created via my REST API, a UserAccountCreatedEvent is stored in the database. A scheduled task checks the database every 5 seconds for new UserAccountCreatedEvents and if one is present, sends an email to the registered user. When running my tests I encounter the problem that the table for the UserAccountCreatedEvent can't be found (see exception below). I used to send the email in a blocking manner in the service method, but I recently switched to this async approach. All my tests worked perfectly for the blocking approach and the only thing I changed for the async approach is to include Awaitility in the test.
2019-04-23 11:24:51.605 ERROR 7968 --- [taskScheduler-1] o.s.s.s.TaskUtils$LoggingErrorHandler : Unexpected error occurred in scheduled task.
org.springframework.dao.InvalidDataAccessResourceUsageException: could not prepare statement; SQL [select useraccoun0_.id as id1_0_, useraccoun0_.completed_at as complete2_0_, useraccoun0_.created_at as created_3_0_, useraccoun0_.in_process_since as in_proce4_0_, useraccoun0_.status as status5_0_, useraccoun0_.user_id as user_id1_35_ from user_account_created_event useraccoun0_ where useraccoun0_.status=? order by useraccoun0_.created_at asc limit ?]; nested exception is org.hibernate.exception.SQLGrammarException: could not prepare statement
Caused by: org.h2.jdbc.JdbcSQLSyntaxErrorException:
Table "USER_ACCOUNT_CREATED_EVENT" not found; SQL statement:
select useraccoun0_.id as id1_0_, useraccoun0_.completed_at as complete2_0_, useraccoun0_.created_at as created_3_0_, useraccoun0_.in_process_since as in_proce4_0_, useraccoun0_.status as status5_0_, useraccoun0_.user_id as user_id1_35_ from user_account_created_event useraccoun0_ where useraccoun0_.status=? order by useraccoun0_.created_at asc limit ? [42102-199]
Full stack trace
Second problem
As if that were not enough, the tests behave completely different when running them in debug mode. When I set a breakpoint in the method that is called by the method which is annotated with #Scheduled, it is invoked several times althogh #Scheduled is configured with a fixedDelayString (fixed delay) of 5000ms. Thanks to logging I can even see that several mails were sent. Still, my test SMTP sever (GreenMail) does not receive any emails. How is this even possible? I've intentionally set the transaction isolation to Isolation.SERIALIZABLE so that it should be impossible (as far as I understand transaction isolation) that two scheduled methods access the same Event from the database.
Third problem
To cap it all, when I rerun the failed tests, THEY WORK. But, there are different exceptions on the console (see below). But still, the app starts and the tests finish successfully. There are different test results depending on if I run all tests vs. only the class vs. only the method vs. rerun failed tests. I don't understand how such an indeterministic behaviour can be possible.
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class]: Invocation of init method failed; nested exception is javax.persistence.PersistenceException: Failed to scan classpath for unlisted entity classes
Caused by: java.nio.channels.ClosedByInterruptException: null
Full stack trace
My code
Test class (UserRegistrationTest)
#ActiveProfiles("test")
#AutoConfigureMockMvc
#RunWith(SpringRunner.class)
#SpringBootTest
#DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD)
public class UserRegistrationTest {
#Autowired
private MockMvc mockMvc;
#Autowired
private ObjectMapper objectMapper;
#Autowired
private Routes routes;
#Autowired
private TestConfig testConfig;
#Resource(name = "validCustomerDTO")
private CustomerDTO validCustomerDTO;
#Resource(name = "validVendorDTO")
private VendorRegistrationDTO validVendorRegistrationDTO;
#Value("${schedule.sendRegistrationConfirmationEmailTaskDelay}")
private Short registrationConfirmationEmailSenderTaskDelay;
private GreenMail smtpServer;
// Setup & tear down
#Before
public void setUp() {
smtpServer = testConfig.getMailServer();
smtpServer.start();
}
#After
public void tearDown() {
smtpServer.stop();
}
// Tests
#Test
public void testCreateCustomerAccount() throws Exception {
mockMvc.perform(
post(routes.getCustomerPath())
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(objectMapper.writeValueAsString(validCustomerDTO)))
.andExpect(status().isCreated());
// When run normally, I get a timeout from the next line
await().atMost(registrationConfirmationEmailSenderTaskDelay + 10000, MILLISECONDS).until(smtpServerReceivedOneEmail());
// Verify correct registration confirmation email was sent
MimeMessage[] receivedMessages = smtpServer.getReceivedMessages();
assertThat(receivedMessages).hasSize(1);
// other checks
// ...
}
#Test
public void testCreateVendorAccount() throws Exception {
mockMvc.perform(
post(routes.getVendorPath())
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(objectMapper.writeValueAsString(validVendorRegistrationDTO)))
.andExpect(status().isCreated());
// When run normally, I get a timeout from the next line
await().atMost(registrationConfirmationEmailSenderTaskDelay + 10000, MILLISECONDS).until(smtpServerReceivedOneEmail());
// Verify correct registration confirmation email was sent
MimeMessage[] receivedMessages = smtpServer.getReceivedMessages();
assertThat(receivedMessages).hasSize(1);
// other checks
// ...
}
// Helper methods
private Callable<Boolean> smtpServerReceivedOneEmail() {
return () -> smtpServer.getReceivedMessages().length == 1;
}
// Test configuration
#TestConfiguration
static class TestConfig {
private static final int PORT = 3025;
private static final String HOST = "localhost";
private static final String PROTOCOL = "smtp";
GreenMail getMailServer() {
return new GreenMail(new ServerSetup(PORT, HOST, PROTOCOL));
}
#Bean
public JavaMailSender javaMailSender() {
JavaMailSenderImpl javaMailSender = new JavaMailSenderImpl();
javaMailSender.setHost(HOST);
javaMailSender.setPort(PORT);
javaMailSender.setProtocol(PROTOCOL);
javaMailSender.setDefaultEncoding("UTF-8");
return javaMailSender;
}
}
Task scheduler (BusinessTaskScheduler)
#Component
public class BusinessTaskScheduler {
private final RegistrationTask registrationTask;
#Autowired
public BusinessTaskScheduler(RegistrationTask registrationTask) {
this.registrationTask = registrationTask;
}
#Scheduled(fixedDelayString = "${schedule.sendRegistrationConfirmationEmailTaskDelay}")
public void sendRegistrationConfirmationEmail() {
registrationTask.sendRegistrationConfirmationEmail();
}
}
The code that is called by the scheduled method (RegistrationTask)
#Component
#Transactional(isolation = Isolation.SERIALIZABLE)
public class RegistrationTask {
private final EmailHelper emailHelper;
private final EventService eventService;
private final UserRegistrationService userRegistrationService;
#Autowired
public RegistrationTask(EmailHelper emailHelper, EventService eventService, UserRegistrationService userRegistrationService) {
this.emailHelper = emailHelper;
this.eventService = eventService;
this.userRegistrationService = userRegistrationService;
}
public void sendRegistrationConfirmationEmail() {
Optional<UserAccountCreatedEvent> optionalEvent = eventService.getOldestUncompletedUserAccountCreatedEvent();
if (optionalEvent.isPresent()) {
UserAccountCreatedEvent event = optionalEvent.get();
User user = event.getUser();
RegistrationVerificationToken token = userRegistrationService.createRegistrationVerificationTokenForUser(user);
emailHelper.sendRegistrationConfirmationEmail(token);
eventService.completeEvent(event);
}
}
}
The event service (EventServiceImpl)
#Service
#Transactional(isolation = Isolation.SERIALIZABLE)
public class EventServiceImpl implements EventService {
private final ApplicationEventDAO applicationEventDAO;
private final UserAccountCreatedEventDAO userAccountCreatedEventDAO;
#Autowired
public EventServiceImpl(ApplicationEventDAO applicationEventDAO, UserAccountCreatedEventDAO userAccountCreatedEventDAO) {
this.applicationEventDAO = applicationEventDAO;
this.userAccountCreatedEventDAO = userAccountCreatedEventDAO;
}
#Override
public void completeEvent(ApplicationEvent event) {
if (!event.getStatus().equals(COMPLETED) && Objects.isNull(event.getCompletedAt())) {
event.setStatus(COMPLETED);
event.setCompletedAt(LocalDateTime.now());
applicationEventDAO.save(event);
}
}
#Override
public Optional<UserAccountCreatedEvent> getOldestUncompletedUserAccountCreatedEvent() {
Optional<UserAccountCreatedEvent> optionalEvent = userAccountCreatedEventDAO.findFirstByStatusOrderByCreatedAtAsc(NEW);
if (optionalEvent.isPresent()) {
UserAccountCreatedEvent event = optionalEvent.get();
setEventInProcess(event);
return Optional.of(userAccountCreatedEventDAO.save(event));
}
return Optional.empty();
}
#Override
public void publishEvent(ApplicationEvent event) {
applicationEventDAO.save(event);
}
// Helper methods
private void setEventInProcess(ApplicationEvent event) {
event.setStatus(Status.IN_PROCESS);
event.setInProcessSince(LocalDateTime.now());
}
}
The UserAccountCreatedEvent
application.yml
schedule:
sendRegistrationConfirmationEmailTaskDelay: 5000 # delay between tasks in milliseconds
I am new to scheduling with Spring, so any help is greatly appreciated!

Spring state machine configuration for rest service requests

I have a simple order processing application and trying to apply spring state machine for handling states of the order.
And I wondering how can I handle order's states for the same order during multiple request from rest service.
Order states:
enum OrderEvents {
FULFILL,
PAY,
CANCEL
}
Order events:
enum OrderStates {
SUBMITTED,
PAID,
FULFILLED,
CANCELLED
}
State machine configuration:
#Log
#Configuration
#EnableStateMachineFactory
class SimpleEnumStatemachineConfiguration extends StateMachineConfigurerAdapter<OrderStates, OrderEvents> {
#Override
public void configure(StateMachineTransitionConfigurer<OrderStates, OrderEvents> transitions) throws Exception {
transitions
.withExternal().source(OrderStates.SUBMITTED).target(OrderStates.PAID).event(OrderEvents.PAY)
.and()
.withExternal().source(OrderStates.PAID).target(OrderStates.FULFILLED).event(OrderEvents.FULFILL)
.and()
.withExternal().source(OrderStates.SUBMITTED).target(OrderStates.CANCELLED).event(OrderEvents.CANCEL)
.and()
.withExternal().source(OrderStates.PAID).target(OrderStates.CANCELLED).event(OrderEvents.CANCEL);
}
#Override
public void configure(StateMachineStateConfigurer<OrderStates, OrderEvents> states) throws Exception {
states
.withStates()
.initial(OrderStates.SUBMITTED)
.state(OrderStates.PAID)
.end(OrderStates.FULFILLED)
.end(OrderStates.CANCELLED);
}
#Override
public void configure(StateMachineConfigurationConfigurer<OrderStates, OrderEvents> config) throws Exception {
config.withConfiguration()
.autoStartup(true)
}
}
In my order service I call
StateMachine<OrderStates, OrderEvents> sm = this.factory.getStateMachine(orderIdKey);
But it seems that every time is new state machine created even for the same orderIdKey. So, how can I get access to state machine created when order was submitted on next state?
You have basically two options:
a) persist the state machine for the given orderId, using a state machine persister as explained here.
b) create a new state machine for given orderId (per HTTP request) and rehydrate the SM state based on the state of the order entity for the given orderId. SM objects are considered lightweight, so this is a viable approach as well. Below is a code sample:
StateMachine<Status, Event> build(long orderId) {
orderService.getOrder(orderId) //returns Optional
.map(order -> {
StateMachine<Status, Event> sm = stateMachineFactory.getStateMachine(Long.toString(orderId));
sm.stop();
rehydrateState(sm, sm.getExtendedState, order.getStatus());
sm.start();
return sm;
})
.orElseGet(() -> createNewStateMachine(orderId);
}
void rehydrateState(StateMachine<Status, Event> newStateMachine, ExtendedState extendedState, Status orderStatus) {
newStateMachine.getStateMachineAccessor().doWithAllRegions(sma ->
sma.resetStateMachine(new DefaultStateMachineContext<>(orderStatus, null, null, extendedState));
});
}

Mockito: Verifying a method was called with a functional parameter

I have a simple scenario in which am trying to verify some behavior when a method is called (i.e. that a certain method was called with given parameter, a function pointer in this scenario). Below are my classes:
#SpringBootApplication
public class Application {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(Application.class, args);
AppBootStrapper bootStrapper = context.getBean(AppBootStrapper.class);
bootStrapper.start();
}
}
#Component
public class AppBootStrapper {
private NetworkScanner networkScanner;
private PacketConsumer packetConsumer;
public AppBootStrapper(NetworkScanner networkScanner, PacketConsumer packetConsumer) {
this.networkScanner = networkScanner;
this.packetConsumer = packetConsumer;
}
public void start() {
networkScanner.addConsumer(packetConsumer::consumePacket);
networkScanner.startScan();
}
}
#Component
public class NetworkScanner {
private List<Consumer<String>> consumers = new ArrayList<>();
public void startScan(){
Executors.newSingleThreadExecutor().submit(() -> {
while(true) {
// do some scanning and get/parse packets
consumers.forEach(consumer -> consumer.accept("Package Data"));
}
});
}
public void addConsumer(Consumer<String> consumer) {
this.consumers.add(consumer);
}
}
#Component
public class PacketConsumer {
public void consumePacket(String packet) {
System.out.println("Packet received: " + packet);
}
}
#RunWith(JUnit4.class)
public class AppBootStrapperTest {
#Test
public void start() throws Exception {
NetworkScanner networkScanner = mock(NetworkScanner.class);
PacketConsumer packetConsumer = mock(PacketConsumer.class);
AppBootStrapper appBootStrapper = new AppBootStrapper(networkScanner, packetConsumer);
appBootStrapper.start();
verify(networkScanner).addConsumer(packetConsumer::consumePacket);
verify(networkScanner, times(1)).startScan();
}
}
I want to verify that bootStrapper did in fact do proper setup by registering the packet consumer(there might be other consumers registered later on, but this one is mandatory) and then called startScan. I get the following error message when I execute the test case:
Argument(s) are different! Wanted:
networkScanner bean.addConsumer(
com.spring.starter.AppBootStrapperTest$$Lambda$8/438123546#282308c3
);
-> at com.spring.starter.AppBootStrapperTest.start(AppBootStrapperTest.java:24)
Actual invocation has different arguments:
networkScanner bean.addConsumer(
com.spring.starter.AppBootStrapper$$Lambda$7/920446957#5dda14d0
);
-> at com.spring.starter.AppBootStrapper.start(AppBootStrapper.java:12)
From the exception, clearly the function pointers aren't the same.
Am I approaching this the right way? Is there something basic I am missing? I played around and had a consumer injected into PacketConsumer just to see if it made a different and that was OK, but I know that's certainly not the right way to go.
Any help, perspectives on this would be greatly appreciated.
Java doesn't have any concept of "function pointers"; when you see:
networkScanner.addConsumer(packetConsumer::consumePacket);
What Java actually compiles is (the equivalent of):
networkScanner.addConsumer(new Consumer<String>() {
#Override void accept(String packet) {
packetConsumer.consumePacket(packet);
}
});
This anonymous inner class happens to be called AppBootStrapper$$Lambda$7. Because it doesn't (and shouldn't) define an equals method, it will never be equal to the anonymous inner class that the compiler generates in your test, which happens to be called AppBootStrapperTest$$Lambda$8. This is regardless of the fact that the method bodies are the same, and are built in the same way from the same method reference.
If you generate the Consumer explicitly in your test and save it as a static final Consumer<String> field, then you can pass that reference in the test and compare it; at that point, reference equality should hold. This should work with a lambda expression or method reference just fine.
A more apt test would probably verify(packetConsumer, atLeastOnce()).consumePacket(...), as the contents of the lambda are an implementation detail and you're really more concerned about how your component collaborates with other components. The abstraction here should be at the consumePacket level, not at the addConsumer level.
See the comments and answer on this SO question.

What could cause a class implementing "ApplicationListener<ContextRefreshedEvent>" not to be notified of a "ContextRefreshedEvent"

I have a Spring application listener implementing ApplicationListener<ContextRefreshedEvent> as follows:
#Profile({ Profiles.DEFAULT, Profiles.CLOUD, Profiles.TEST, Profiles.DEV })
#Component
public class BootstrapLoaderListener implements ApplicationListener<ContextRefreshedEvent>, ResourceLoaderAware, Ordered {
private static final Logger log = Logger.getLogger(BootstrapLoaderListener.class);
#Override
public int getOrder() {
return HIGHEST_PRECEDENCE;
}
#Autowired
private DayToTimeSlotRepository dayToTimeSlotRepository;
#Autowired
private LanguageRepository languageRepository;
private ResourceLoader resourceLoader;
#Override
#Transactional
public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
initApplication();
}
private void initApplication() {
if (dayToTimeSlotRepository.count() == 0) {
initDayToTimeSlots();
}
if (languageRepository.count() == 0) {
initLanguages();
}
}
private void initDayToTimeSlots() {
for (Day day : Day.values()) {
for (TimeSlot timeSlot : TimeSlot.values()) {
DayToTimeSlot dayToTimeSlot = new DayToTimeSlot();
dayToTimeSlot.setDay(day);
dayToTimeSlot.setTimeSlot(timeSlot);
dayToTimeSlot.setDisabled(isDayToTimeSlotDisabled(timeSlot, day));
dayToTimeSlotRepository.save(dayToTimeSlot);
}
}
}
...
I rely on this listener class to insert reference data that is not updated nor deleted and I have a number of Spring integration tests that use this class, one of which fails because the listener is not notified (initDayToTimeSlots is not invoked).
I am trying to pinpoint where the problem comes from by debugging the tests and I noticed that when I run the problematic test class on its own, the tests contained in the class pass (indicating that the listener is notified) but when I run all of my application test classes together, the listener is not notified causing the test to fail (indicating that some other test changes/dirties the context).
Here is the problematic test class:
#ActiveProfiles({ Profiles.TEST })
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration(classes = { FullIntegrationTestConfiguration.class, BaseTestConfiguration.class })
public class RegularDayToTimeSlotsTest {
private static int NUMBER_OF_REGULAR_DAY_TO_TIME_SLOTS_IN_WEEK = 25;
#Before
public void setup() {
//org.hsqldb.util.DatabaseManagerSwing.main(new String[] { "--url", "jdbc:hsqldb:mem:bignibou", "--noexit" });
}
#Autowired
private AdvertisementService advertisementService;
#Test
public void shouldNotContainSaturdayNorSunday() {
Set<DayToTimeSlot> regularDayToTimeSlots = advertisementService.retrieveRegularDayToTimeSlots();
assertThat(regularDayToTimeSlots).onProperty("day").excludes(Day.SATURDAY, Day.SUNDAY);
assertThat(regularDayToTimeSlots).onProperty("day").contains(Day.MONDAY, Day.THUESDAY);
}
#Test
public void shouldNotContainEveningNorNighttime() {
Set<DayToTimeSlot> regularDayToTimeSlots = advertisementService.retrieveRegularDayToTimeSlots();
assertThat(regularDayToTimeSlots).onProperty("timeSlot").excludes(TimeSlot.EVENING, TimeSlot.NIGHTTIME);
assertThat(regularDayToTimeSlots).onProperty("timeSlot").contains(TimeSlot.MORNING, TimeSlot.LUNCHTIME);
}
#Test
public void shouldContainCorrectNumberOfDayToTimeSlots() {
Set<DayToTimeSlot> regularDayToTimeSlots = advertisementService.retrieveRegularDayToTimeSlots();
assertThat(regularDayToTimeSlots).hasSize(NUMBER_OF_REGULAR_DAY_TO_TIME_SLOTS_IN_WEEK);
}
}
I am puzzled to see that both the prepareRefresh() and finishRefresh() methods within AbstractApplicationContext.refresh method are indeed called but that my listener is not notified...
Has anyone got any clue?
P.S. I know I could use #DirtiesContext in order to get a fresh context and I also know it would be preferable not to rely on an application listener for my tests but I am very anxious to understand what is going wrong here. Hence this post.
edit 1: When I debug the problematic test class in isolation, I notice that the event source is of type GenericApplicationContext and as explained above the test passes OK because the listener is notified. However when all test classes are run together, the event source is, oddly enough, of type GenericWebApplicationContext and no listener is found here in SimpleApplicationEventMulticaster:
#Override
public void multicastEvent(final ApplicationEvent event) {
for (final ApplicationListener<?> listener : getApplicationListeners(event)) {
Executor executor = getTaskExecutor();
if (executor != null) {
executor.execute(new Runnable() {
#Override
public void run() {
invokeListener(listener, event);
}
});
}
else {
invokeListener(listener, event);
}
}
}
edit 2: my comments in edit 1 make me asks myself what is responsible for determining the uniqueness of context configuration...
For instance, I have only two test classes with the following context configuration:
#ContextConfiguration(classes = { FullIntegrationTestConfiguration.class, BaseTestConfiguration.class })
I guess they both will use the same cached context, won't they? Now can a third class use the same cached context even though it does not have exactly the same context configuration?
Why does my test get a GenericWebApplicationContext above?
my comments in edit 1 make me asks myself what is responsible for
determining the uniqueness of context configuration...
The elements that make up the context cache key are described in the Context caching section of the "Testing" chapter in the reference manual.
For instance, I have only two test classes with the following context
configuration:
#ContextConfiguration(classes = {
FullIntegrationTestConfiguration.class, BaseTestConfiguration.class })
I guess they both will use the same cached context, won't they?
If they declare only those two configuration classes in that exact order, then yes.
Now can a third class use the same cached context even though it does not
have exactly the same context configuration?
No.
Why does my test get a GenericWebApplicationContext above?
A GenericWebApplicationContext is only loaded if your test class (or one of its superclasses) is annotated with #WebAppConfiguration.
If you are experiencing behavior that contradicts this, then you have discovered a bug in which case we would appreciate it if you could produce a scaled down test project in the issue repository and create a corresponding JIRA issue against the "Spring Framework" and its "Test" component.
Thanks,
Sam (author of the Spring TestContext Framework)

Unit Testing Spring ApplicationEvents - Events are getting published but the listeners aren't firing?

I'm trying to unit test the custom events that I've created in Spring and am running into an interesting problem. If I create a StaticApplicationContext and manually register and wire the beans I can trigger events and see the program flow through the publisher (implements ApplicationEventPublisherAware) through to the listener (implements ApplicationListener<?>).
Yet when I try to create a JUnit test to create the context using the SpringJunit4ClassRunner and #ContextConfiguration everything works well except that the ApplicationEvents are not showing up in the listener (I have confirmed that they are getting published).
Is there some other way to create the context so that ApplicationEvents will work correctly? I haven't found much on the web about unit testing the Spring events framework.
The events will not fire because your test classes are not registered and resolved from the spring application context, which is the event publisher.
I've implemented a workaround for this where the event is handled in another class that is registered with Spring as a bean and resolved as part of the test. It isn't pretty, but after wasting the best part of a day trying to find a better solution I am happy with this for now.
My use case was firing an event when a message is received within a RabbitMQ consumer. It is made up of the following:
The wrapper class
Note the Init() function that is called from the test to pass in the callback function after resolving from the container within the test
public class TestEventListenerWrapper {
CountDownLatch countDownLatch;
TestEventWrapperCallbackFunction testEventWrapperCallbackFunction;
public TestEventListenerWrapper(){
}
public void Init(CountDownLatch countDownLatch, TestEventWrapperCallbackFunction testEventWrapperCallbackFunction){
this.countDownLatch = countDownLatch;
this.testEventWrapperCallbackFunction = testEventWrapperCallbackFunction;
}
#EventListener
public void onApplicationEvent(MyEventType1 event) {
testEventWrapperCallbackFunction.CallbackOnEventFired(event);
countDownLatch.countDown();
}
#EventListener
public void onApplicationEvent(MyEventType2 event) {
testEventWrapperCallbackFunction.CallbackOnEventFired(event);
countDownLatch.countDown();
}
#EventListener
public void onApplicationEvent(OnQueueMessageReceived event) {
testEventWrapperCallbackFunction.CallbackOnEventFired(event);
countDownLatch.countDown();
}
}
The callback interface
public interface TestEventWrapperCallbackFunction {
void CallbackOnEventFired(ApplicationEvent event);
}
A test configuration class to define the bean which is referenced in the unit test. Before this is useful, it will need to be resolved from the applicationContext and initialsed (see next step)
#Configuration
public class TestContextConfiguration {
#Lazy
#Bean(name="testEventListenerWrapper")
public TestEventListenerWrapper testEventListenerWrapper(){
return new TestEventListenerWrapper();
}
}
Finally, the unit test itself that resolves the bean from the applicationContext and calls the Init() function to pass assertion criteria (this assumes you have registered the bean as a singleton - the default for the Spring applicationContext). The callback function is defined here and also passed to Init().
#ContextConfiguration(classes= {TestContextConfiguration.class,
//..., - other config classes
//..., - other config classes
})
public class QueueListenerUnitTests
extends AbstractTestNGSpringContextTests {
private MessageProcessorManager mockedMessageProcessorManager;
private ChannelAwareMessageListener queueListener;
private OnQueueMessageReceived currentEvent;
#BeforeTest
public void Startup() throws Exception {
this.springTestContextPrepareTestInstance();
queueListener = new QueueListenerImpl(mockedMessageProcessorManager);
((QueueListenerImpl) queueListener).setApplicationEventPublisher(this.applicationContext);
currentEvent = null;
}
#Test
public void HandleMessageReceived_QueueMessageReceivedEventFires_WhenValidMessageIsReceived() throws Exception {
//Arrange
//Other arrange logic
Channel mockedRabbitmqChannel = CreateMockRabbitmqChannel();
CountDownLatch countDownLatch = new CountDownLatch(1);
TestEventWrapperCallbackFunction testEventWrapperCallbackFunction = (ev) -> CallbackOnEventFired(ev);
TestEventListenerWrapper testEventListenerWrapper = (TestEventListenerWrapper)applicationContext.getBean("testEventWrapperOnQueueMessageReceived");
testEventListenerWrapper.Init(countDownLatch, testEventWrapperCallbackFunction);
//Act
queueListener.onMessage(message, mockedRabbitmqChannel);
long awaitTimeoutInMs = 1000;
countDownLatch.await(awaitTimeoutInMs, TimeUnit.MILLISECONDS);
//Assert - assertion goes here
}
//The callback function that passes the event back here so it can be made available to the tests for assertion
private void CallbackOnEventFired(ApplicationEvent event){
currentEvent = (OnQueueMessageReceived)event;
}
}
EDIT 1: The sample code has been updated with CountDownLatch
EDIT 2: Assertions didn't fail tests so the above was updated with a different approach**
I just run my app as SpringBootTest, application events working fine:
#TestComponent
public class EventTestListener {
#EventListener
public void handle(MyCustomEvent event) {
// nothing to do, just spy the method...
}
}
#RunWith(SpringRunner.class)
#SpringBootTest
public class MyEventTest {
#SpyBean
private EventTestListener testEventListener;
#Test
public void testMyEventFires() {
// do something that fires the event..
verify(testEventListener).handle(any(MyCustomEvent.class));
}
}
use the #Captor / ArgumentCaptor to verify the content of your event.
You can create a context manually.
For example: I had needed to check if my ApplicationListener<ContextClosedEvent> closed Cassandra connections:
#Test
public void testSpringShutdownHookForCassandra(){
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(CassandraConfig.class);
CassandraConnectionManager connectionManager = ctx.getBean(CassandraConnectionManager.class);
Session session = connectionManager.openSession(testKeySpaceName);
Assert.assertFalse( session.isClosed() );
ctx.close();
Assert.assertTrue( session.isClosed() );
}

Resources