Hands‑On Unit Testing of Spring’s Full Event‑Driven Architecture
This guide walks through comprehensive unit tests for Spring Boot 3.x / Spring Framework 6.x event‑driven features—covering @EventListener, ApplicationListener, @Order, @Async, TransactionalEventListener, exception propagation, multiple event publishing, POJO events, and a comparison of three asynchronous handling approaches—using JDK 17+, JUnit 5, and Spring Boot Test.
Version requirements : Spring Boot 3.x, Spring Framework 6.x, JDK 17+. Test framework: JUnit 5 + Spring Boot Test.
Environment setup
Maven dependencies needed for the tests:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.4</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>Test classes
SpringEventTest – Event‑driven feature tests
This class validates nine scenarios:
@EventListener synchronous handling
ApplicationListener interface handling
@Order controlling listener execution order
@Async asynchronous handling (direct bean call)
TransactionalEventListener transaction events
Exception propagation in synchronous mode (twice listed)
Multiple event publishing with incremental IDs
POJO event objects without extending ApplicationEvent
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
class SpringEventTest {
@Autowired private EventOrderService eventOrderService;
@Autowired private TxOrderService txOrderService;
@Autowired private AsyncOrderProcessor asyncOrderProcessor;
@Autowired private TransactionTemplate transactionTemplate;
@BeforeEach
void setUp() {
OrderSequenceListener.clearLog();
AsyncOrderProcessor.clearLog();
TransactionalOrderListener.clearLog();
ExceptionThrowingListener.disable();
}
@Test @DisplayName("Test 1: @EventListener synchronous handling")
void testSyncEventListener() {
int orderId = eventOrderService.createOrder("Alice");
assertTrue(orderId > 0, "orderId should be > 0");
System.out.println("Test1 passed, orderId=" + orderId);
}
@Test @DisplayName("Test 2: ApplicationListener interface handling")
void testApplicationListenerInterface() {
int orderId = eventOrderService.createOrder("Bob");
assertTrue(orderId > 0);
System.out.println("Test2 passed, orderId=" + orderId);
}
@Test @DisplayName("Test 3: @Order execution order")
void testOrderAnnotation() {
int orderId = eventOrderService.createOrder("Charlie");
assertTrue(orderId > 0);
List<String> log = OrderSequenceListener.executionLog;
assertTrue(log.size() >= 2);
assertTrue(log.get(0).contains("OrderSequenceListener"));
assertTrue(log.get(1).contains("OrderSecondListener"));
System.out.println("Test3 passed, order execution order verified");
}
@Test @DisplayName("Test 4: @Async asynchronous handling")
void testAsyncEventListener() throws Exception {
String publishThread = Thread.currentThread().getName();
int orderId = eventOrderService.createOrder("AsyncUser");
TimeUnit.MILLISECONDS.sleep(500);
assertTrue(orderId > 0);
assertTrue(AsyncOrderProcessor.asyncLog.size() >= 1);
String asyncThread = extractThreadName(AsyncOrderProcessor.asyncLog.get(0));
assertNotEquals(publishThread, asyncThread, "async method should run in a different thread");
System.out.println("Test4 passed, async processing on thread " + asyncThread);
}
// ... tests 5‑8 omitted for brevity but follow the same pattern ...
private String extractThreadName(String log) {
int idx = log.lastIndexOf("thread=");
if (idx >= 0) return log.substring(idx + "thread=".length());
return log;
}
}AsyncEventComparisonTest – Comparing three async approaches
Three ways are demonstrated:
Way 1: @EventListener + @Async (unreliable)
Way 2: Global task executor configuration (affects all listeners)
Way 3: Synchronous listener that calls an @Async bean method (recommended)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
class AsyncEventComparisonTest {
@Autowired private EventOrderService eventOrderService;
@Autowired private AsyncOrderProcessor asyncOrderProcessor;
@BeforeEach
void setUp() {
UnreliableAsyncListener.clearLog();
AsyncOrderProcessor.clearLog();
RecommendedAsyncListener.clearLog();
}
@Test @DisplayName("Way1: @EventListener + @Async (unreliable)")
void testWay1_UnreliableAsyncAnnotation() throws Exception {
String mainThread = Thread.currentThread().getName();
int orderId = eventOrderService.createOrder("Way1User");
TimeUnit.MILLISECONDS.sleep(500);
List<String> log = UnreliableAsyncListener.executionLog;
System.out.println("Way1 main thread: " + mainThread);
if (!log.isEmpty()) {
String listenerThread = extractThreadName(log.get(0));
boolean isAsync = !mainThread.equals(listenerThread);
System.out.println("Is async: " + (isAsync ? "✅" : "❌"));
}
assertTrue(orderId > 0);
}
@Test @DisplayName("Way2: Global task executor (simple config, all listeners async)")
void testWay2_GlobalTaskExecutor() {
System.out.println("Enable GlobalAsyncEventConfig and comment out @EnableAsync in AsyncEventConfig.");
assertTrue(true, "configuration example provided");
}
@Test @DisplayName("Way3: Synchronous listener + @Async bean method (recommended)")
void testWay3_RecommendedAsyncCall() throws Exception {
String mainThread = Thread.currentThread().getName();
int orderId = eventOrderService.createOrder("Way3User");
TimeUnit.MILLISECONDS.sleep(500);
List<String> syncLog = RecommendedAsyncListener.executionLog;
List<String> asyncLog = AsyncOrderProcessor.asyncLog;
assertFalse(syncLog.isEmpty(), "sync listener should receive event");
String syncThread = extractThreadName(syncLog.get(0));
assertEquals(mainThread, syncThread, "sync listener runs on main thread");
assertFalse(asyncLog.isEmpty(), "async method should produce a log");
String asyncThread = extractThreadName(asyncLog.get(0));
assertNotEquals(mainThread, asyncThread, "async method runs in a different thread");
System.out.println("Way3 confirmed: sync receipt + async processing");
}
private String extractThreadName(String log) {
int idx = log.lastIndexOf("thread=");
return idx >= 0 ? log.substring(idx + "thread=".length()) : log;
}
}Configuration classes
AsyncEventConfig – Bean‑level async executor
@Configuration
@EnableAsync
public class AsyncEventConfig {
@Bean("eventAsyncExecutor")
public ThreadPoolTaskExecutor eventAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(8);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("async-event-");
executor.initialize();
return executor;
}
}GlobalAsyncEventConfig – Optional global executor (affects @TransactionalEventListener)
@Configuration
public class GlobalAsyncEventConfig {
// Uncomment to enable global async multicaster
// @Bean
// public ApplicationEventMulticaster applicationEventMulticaster() {
// SimpleApplicationEventMulticaster multicaster = new SimpleApplicationEventMulticaster();
// multicaster.setTaskExecutor(globalAsyncExecutor());
// return multicaster;
// }
@Bean("globalAsyncExecutor")
public TaskExecutor globalAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(8);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("global-async-event-");
executor.initialize();
return executor;
}
}Listeners
Key listeners illustrate ordering, async handling, and error propagation.
@Component
@Order(1)
public class OrderSequenceListener implements ApplicationListener<OrderCreatedEvent> {
public static final List<String> executionLog = new CopyOnWriteArrayList<>();
@Override
public void onApplicationEvent(OrderCreatedEvent event) {
String log = "OrderSequenceListener[1] orderId=" + event.getOrderId() + ", thread=" + Thread.currentThread().getName();
executionLog.add(log);
System.out.println(log);
}
public static void clearLog() { executionLog.clear(); }
}
@Component
public class OrderSecondListener {
@EventListener
@Order(2)
public void onOrderCreated(OrderCreatedEvent event) {
String log = "OrderSecondListener[2] orderId=" + event.getOrderId() + ", thread=" + Thread.currentThread().getName();
OrderSequenceListener.executionLog.add(log);
System.out.println(log);
}
// additional @EventListener methods for OrderPayEvent omitted for brevity
}
@Component
public class InventoryUpdateListener {
@EventListener
@Order(3)
public void onOrderCreated(OrderCreatedEvent event) {
System.out.println("[InventoryUpdateListener] deduct stock, orderId=" + event.getOrderId());
}
}
@Component
public class EmailNotificationListener {
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
System.out.println("[EmailNotificationListener] send email to " + event.getUserName() + ", orderId=" + event.getOrderId());
}
}
@Component
public class RecommendedAsyncListener {
private final AsyncOrderProcessor asyncProcessor;
public static final List<String> executionLog = new CopyOnWriteArrayList<>();
public RecommendedAsyncListener(AsyncOrderProcessor asyncProcessor) { this.asyncProcessor = asyncProcessor; }
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
String log = "RecommendedAsyncListener sync orderId=" + event.getOrderId() + ", thread=" + Thread.currentThread().getName();
executionLog.add(log);
System.out.println(log);
asyncProcessor.processOrderAsync(event);
}
public static void clearLog() { executionLog.clear(); }
}
@Component
public class UnreliableAsyncListener {
public static final List<String> executionLog = new CopyOnWriteArrayList<>();
@EventListener
@Async("eventAsyncExecutor")
public void handleOrderEvent(OrderCreatedEvent event) {
String log = "UnreliableAsyncListener orderId=" + event.getOrderId() + ", thread=" + Thread.currentThread().getName();
executionLog.add(log);
System.out.println("[Warning] " + log);
}
public static void clearLog() { executionLog.clear(); }
}
@Component
public class ExceptionThrowingListener {
public static final List<String> errorLog = new CopyOnWriteArrayList<>();
private static volatile boolean enabled = false;
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
if (enabled) {
String msg = "ExceptionThrowingListener throws, orderId=" + event.getOrderId();
errorLog.add(msg);
throw new RuntimeException("Simulated listener exception: " + msg);
}
}
public static void enable() { enabled = true; }
public static void disable() { enabled = false; errorLog.clear(); }
}Business services and events
@Service("eventOrderService")
public class EventOrderService {
private final ApplicationEventPublisher eventPublisher;
private final AtomicInteger idGenerator = new AtomicInteger(0);
public EventOrderService(ApplicationEventPublisher eventPublisher) { this.eventPublisher = eventPublisher; }
public int createOrder(String userName) {
int orderId = idGenerator.incrementAndGet();
System.out.println("[EventOrderService] create orderId=" + orderId + ", user=" + userName);
OrderCreatedEvent event = new OrderCreatedEvent(this, orderId, userName);
eventPublisher.publishEvent(event);
return orderId;
}
public int payOrder(int orderId, String userName) {
System.out.println("[EventOrderService] pay orderId=" + orderId + ", user=" + userName);
OrderPayEvent event = new OrderPayEvent(orderId, userName);
eventPublisher.publishEvent(event);
return orderId;
}
}
@Service("txOrderService")
public class TxOrderService {
private final ApplicationEventPublisher eventPublisher;
private final AtomicInteger idGenerator = new AtomicInteger(1000);
public TxOrderService(ApplicationEventPublisher eventPublisher) { this.eventPublisher = eventPublisher; }
@Transactional
public int createOrderInTransaction(String userName) {
int orderId = idGenerator.incrementAndGet();
OrderCreatedEvent event = new OrderCreatedEvent(this, orderId, userName);
eventPublisher.publishEvent(event);
return orderId;
}
}
public class OrderCreatedEvent extends ApplicationEvent {
private final int orderId;
private final String userName;
public OrderCreatedEvent(Object source, int orderId, String userName) { super(source); this.orderId = orderId; this.userName = userName; }
public int getOrderId() { return orderId; }
public String getUserName() { return userName; }
@Override public String toString() { return "OrderCreatedEvent{orderId=" + orderId + ", userName='" + userName + "'}"; }
}
public class OrderPayEvent {
private final int orderId;
private final String userName;
public OrderPayEvent(int orderId, String userName) { this.orderId = orderId; this.userName = userName; }
public int getOrderId() { return orderId; }
public String getUserName() { return userName; }
@Override public String toString() { return "OrderPayEvent{orderId=" + orderId + ", userName='" + userName + "'}"; }
}Transactional event listener
@Component
public class TransactionalOrderListener {
public static final List<String> txLog = new CopyOnWriteArrayList<>();
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onOrderAfterCommit(OrderCreatedEvent event) {
String log = "TransactionalOrderListener AFTER_COMMIT orderId=" + event.getOrderId() + ", thread=" + Thread.currentThread().getName();
txLog.add(log);
System.out.println(log);
}
@TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
public void onOrderAfterRollback(OrderCreatedEvent event) {
String log = "TransactionalOrderListener AFTER_ROLLBACK orderId=" + event.getOrderId() + ", thread=" + Thread.currentThread().getName();
txLog.add(log);
System.out.println(log);
}
public static void clearLog() { txLog.clear(); }
}Running the tests
Use Maven:
# Run a single test class
mvn test -Dtest=SpringEventTestOr run from IntelliJ IDEA by right‑clicking the test class or method and selecting Run or Debug .
Related documentation
Full analysis of Spring event‑driven mechanisms is available in the article “Spring 事件驱动全解析”.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
CodeSmart Hoops
A working programmer who loves coding and basketball. By day I debug code; by night I dissect tactics. I write articles to document my journey, focusing on Java, AI, Python and other programming topics, with occasional posts about basketball, English, and books. Hope it's helpful—thanks for following and support.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
