如何測試 Spring 應用程式事件
1. 概述
在本教程中,我們將討論使用 Spring 應用程式事件的測試程式碼。我們將首先手動建立測試實用程序,以幫助我們發布和收集應用程式事件以進行測試。
之後,我們將發現 Spring Modulith 的測試庫並使用其流暢的 Scenario API 來討論常見的測試案例。使用這種聲明性 DSL,我們將編寫可以輕鬆產生和使用應用程式事件的表達測試。
2. 應用事件
Spring 框架提供應用程式事件,允許元件相互通信,同時保持鬆散耦合。我們可以使用ApplicationEventPublisher
bean 來發佈內部事件,這些事件是普通的 Java 物件。因此,所有註冊的偵聽器都會收到通知。
例如, OrderService
元件可以在Order
成功下達時發布OrderCompletedEvent
:
@Service
public class OrderService {
private final ApplicationEventPublisher eventPublisher;
// constructor
public void placeOrder(String customerId, String... productIds) {
Order order = new Order(customerId, Arrays.asList(productIds));
// business logic to validate and place the order
OrderCompletedEvent event = new OrderCompletedEvent(savedOrder.id(), savedOrder.customerId(), savedOrder.timestamp());
eventPublisher.publishEvent(event);
}
}
正如我們所看到的,已完成的訂單現在會作為應用程式事件發布。因此,來自不同模組的元件現在可以監聽這些事件並做出相應的反應。
我們假設LoyaltyPointsService
對這些事件做出反應,並以忠誠度積分獎勵客戶。為了實現這一點,我們可以利用 Spring 的@EventListener
註解:
@Service
public class LoyaltyPointsService {
private static final int ORDER_COMPLETED_POINTS = 60;
private final LoyalCustomersRepository loyalCustomers;
// constructor
@EventListener
public void onOrderCompleted(OrderCompletedEvent event) {
// business logic to reward customers
loyalCustomers.awardPoints(event.customerId(), ORDER_COMPLETED_POINTS);
}
}
使用應用程式事件而不是直接方法呼叫使我們能夠保持更鬆散的耦合並反轉兩個模組之間的依賴關係。換句話說, “orders”
模組不具有對「 rewards
」模組中的類別的原始碼依賴性。
3. 測試事件監聽器
我們可以從測試本身發布應用程式事件來測試使用@EventListener
的元件。
要測試LoyaltyPointsService
,我們需要建立一個@SpringBootTest,
注入ApplicationEventPublisher
bean,並使用它來發布OrderCompletedEvent
:
@SpringBootTest
class EventListenerUnitTest {
@Autowired
private LoyalCustomersRepository customers;
@Autowired
private ApplicationEventPublisher testEventPublisher;
@Test
void whenPublishingOrderCompletedEvent_thenRewardCustomerWithLoyaltyPoints() {
OrderCompletedEvent event = new OrderCompletedEvent("order-1", "customer-1", Instant.now());
testEventPublisher.publishEvent(event);
// assertions
}
}
最後,我們需要斷言LoyaltyPointsService
消耗了該事件並以正確的積分獎勵客戶。讓我們使用LoyalCustomersRepository
來查看該客戶獲得了多少忠誠度積分:
@Test
void whenPublishingOrderCompletedEvent_thenRewardCustomerWithLoyaltyPoints() {
OrderCompletedEvent event = new OrderCompletedEvent("order-1", "customer-1", Instant.now());
testEventPublisher.publishEvent(event);
assertThat(customers.find("customer-1"))
.isPresent().get()
.hasFieldOrPropertyWithValue("customerId", "customer-1")
.hasFieldOrPropertyWithValue("points", 60);
}
如預期,測試通過:「 rewards
」模組接收並處理事件,並應用獎金。
4. 測試事件發布者
我們可以透過在測試包中建立自訂事件監聽器來測試發布應用程式事件的元件。該監聽器還將使用@EventHandler
註釋,類似於生產實作。不過,這次我們將把所有傳入事件收集到一個清單中,該清單將透過 getter 公開:
@Component
class TestEventListener {
final List<OrderCompletedEvent> events = new ArrayList<>();
// getter
@EventListener
void onEvent(OrderCompletedEvent event) {
events.add(event);
}
void reset() {
events.clear();
}
}
正如我們所觀察到的,我們也可以加入實用程式reset().
我們可以在每次測試之前調用它,以清除前一個測試產生的事件。讓我們建立 Spring Boot 測試和@Autowire
我們的TestEventListener
元件:
@SpringBootTest
class EventPublisherUnitTest {
@Autowired
OrderService orderService;
@Autowired
TestEventListener testEventListener;
@BeforeEach
void beforeEach() {
testEventListener.reset();
}
@Test
void whenPlacingOrder_thenPublishApplicationEvent() {
// place an order
assertThat(testEventListener.getEvents())
// verify the published events
}
}
為了完成測試,我們需要使用OrderService
元件下訂單。之後,我們將斷言testEventListener
恰好接收到一個應用程式事件,並具有足夠的屬性:
@Test
void whenPlacingOrder_thenPublishApplicationEvent() {
orderService.placeOrder("customer1", "product1", "product2");
assertThat(testEventListener.getEvents())
.hasSize(1).first()
.hasFieldOrPropertyWithValue("customerId", "customer1")
.hasFieldOrProperty("orderId")
.hasFieldOrProperty("timestamp");
}
如果我們仔細觀察,我們會發現這兩個測試的設定和驗證是相輔相成的。此測試模擬方法呼叫並偵聽已發布的事件,而前一個測試則發布事件並驗證狀態變更。換句話說,我們只使用兩個測試來測試整個過程:每個測試會覆蓋一個不同的段,在邏輯模組邊界處分割。
5. Spring Modulith的測試支持
Spring Modulith 提供了一系列可以獨立使用的工件。這些庫提供了一系列功能,主要旨在在應用程式內的邏輯模組之間建立清晰的界限。
5.1.場景API
這種架構風格透過利用應用程式事件來促進模組之間的靈活互動。因此, Spring Modulith 中的工件之一為涉及應用程式事件的測試流程提供支援。
讓我們將spring-modulith-starter-test
maven 依賴項加入pom.xml
:
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-starter-test</artifactId>
<version>1.1.3</version>
</dependency>
這允許我們使用 Scenario API 以聲明性方式編寫測試。首先,我們將建立一個測試類別並使用@ApplcationModuleTest.
因此,我們將能夠在任何測試方法中註入Scenario
物件:
@ApplicationModuleTest
class SpringModulithScenarioApiUnitTest {
@Test
void test(Scenario scenario) {
// ...
}
}
簡而言之,此功能提供了方便的 DSL,使我們能夠測試最常見的用例。例如,它可以透過以下方式輕鬆啟動測試並評估其結果:
- 執行方法調用
- 發布應用程式事件
- 驗證狀態變化
- 捕獲並驗證傳出事件
此外,該 API 還提供了一些其他實用程序,例如:
- 輪詢和等待非同步應用程式事件
- 定義逾時
- 對捕獲的事件進行過濾和映射
- 建立自訂斷言
5.2.使用場景 API 測試事件監聽器
要使用@EventListener
方法來測試元件,我們必須注入ApplicationEventPublisher
bean 並發布OrderCompletedEvent
。然而,Spring Modulith 的測試 DSL 透過scenario.publish():
@Test
void whenReceivingPublishOrderCompletedEvent_thenRewardCustomerWithLoyaltyPoints(Scenario scenario) {
scenario.publish(new OrderCompletedEvent("order-1", "customer-1", Instant.now()))
.andWaitForStateChange(() -> loyalCustomers.find("customer-1"))
.andVerify(it -> assertThat(it)
.isPresent().get()
.hasFieldOrPropertyWithValue("customerId", "customer-1")
.hasFieldOrPropertyWithValue("points", 60));
}
andWaitforStateChange()
方法接受 lambda 表達式,並重試執行該表達式,直到傳回非null
物件或非空Optional
。此機制對於非同步方法呼叫特別有用。
總之,我們定義了一個scenario
,在其中發布事件,等待狀態更改,然後驗證系統的最終狀態。
5.3.使用場景 API 測試事件發布者
我們也可以使用Scenario API來模擬方法調用,並攔截和驗證傳出的應用程式事件。讓我們使用 DSL 編寫一個測試來驗證「 order
」模組的行為:
@Test
void whenPlacingOrder_thenPublishOrderCompletedEvent(Scenario scenario) {
scenario.stimulate(() -> orderService.placeOrder("customer-1", "product-1", "product-2"))
.andWaitForEventOfType(OrderCompletedEvent.class)
.toArriveAndVerify(evt -> assertThat(evt)
.hasFieldOrPropertyWithValue("customerId", "customer-1")
.hasFieldOrProperty("orderId")
.hasFieldOrProperty("timestamp"));
}
正如我們所看到的, andWaitforEventOfType()
方法允許我們聲明我們想要捕獲的事件的類型。接下來, toArriveAndVerify()
用於等待事件並執行相關斷言。
六,結論
在本文中,我們了解了使用 Spring 應用程式事件測試程式碼的各種方法。在我們的第一個測試中,我們使用ApplicationEventPublisher
手動發布應用程式事件。
同樣,我們創建了一個自訂TestEventListener
,它使用@EventHandler
註解來擷取所有傳出事件。我們使用這個輔助元件來捕獲和驗證我們的應用程式在測試期間產生的事件。
之後,我們了解了 Spring Modulith 的測試支持,並使用 Scenario API 以聲明式方式編寫相同的測試。 Fluent DSL 允許我們發布和捕獲應用程式事件、模擬方法呼叫以及等待狀態變更。
像往常一樣,完整的源代碼可以在 GitHub 上找到。