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.

CodeSmart Hoops
CodeSmart Hoops
CodeSmart Hoops
Hands‑On Unit Testing of Spring’s Full Event‑Driven Architecture

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=SpringEventTest

Or 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 事件驱动全解析”.

Test result screenshot
Test result screenshot
Async comparison result
Async comparison result
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaSpringunit-testingSpring BootAsyncevent-driven@Transactional
CodeSmart Hoops
Written by

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.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.