詳解Spring云端微服務的組件測試
譯文軟件開發教父--Martin Fowler在其題為《??微服務架構的測試策略??》講演中,詳細詮釋了測試不同級別的微服務的概念,其中就提到了如下圖所示的“測試金字塔”模型。該模型從下到上分別為:單元、集成、組件、端到端和探索。
而不可否認的是,隨著業界廣泛采用云端微服務,我們在得益于處理多個可獨立部署的組件的同時,需要提高微服務應用的測試級別,并按需增加測試策略的復雜性。下面,我將從使用者的角度出發,以一個Spring Cloud微服務為例,深入探究各種服務組件的相關測試。
服務
我們的Spring Boot微服務示例會具有如下特征:
- 將啟用??Spring Cloud Netflix??。它會使用Spring Boot應用的Netflix OSS集成,來執行服務注冊與發現、分布式外部配置(Spring Cloud Config)、以及客戶端的負載平衡
- 將與關系型數據庫(如PostgreSQL)相集成
- 將能調用另一個(內部)微服務
- 將調用第三方(外部)Web服務
- 將啟用Spring Security,以充當OAuth2的資源服務器
- 將被“隱藏”在API網關服務器,例如??Spring Cloud Gateway??的后面
我們將通過Java 11、Apache Maven、Docker、以及一組協作庫,“盡早地”在CI/CD管道中,進行單獨的服務測試,而無需實際部署或占用其他服務、數據庫、甚至是完整的測試環境資源。同時,您可以通過鏈接--https://github.com/kmandalas/spring-cloud-component-tests,在GitHub上獲取該示例的所有代碼。
該示例中的“訂單跟蹤”微服務是由一個Spring Controller、Service和Repository所組成。它公開了兩個端點:
- GET/api/orders/{trackingNumber}/status:它通過給定的跟蹤號,執行數據庫查詢,來獲取相關訂單;然后調用FulfillmentService的內部服務,來確定交付的狀態;進而讓最終外部服務根據狀態,調用位置服務來實現定位。這是一個帶有有效的JWT、且受保護的API調用。
- GET/api/orders:通過查詢數據庫,以列出所有訂單。這是一個受到額外授權限制的、且受保護的API調用。它僅適用于具有“back-office”角色的用戶。
組件測試
OrderControllerTest.java類將針對API提供的多種方法,來封裝組件測試。例如,我們可以選用包括:??Maven插件??、??JUnit功能??、??Spring Boot測試切片??和分類單元測試、集成測試、組件測試、合同測試等方法。當然,并非所有的測試類別都需要在CI/CD管道中被執行(或重新執行)。鑒于該示例過于簡單,我強烈建議您實施適當的分類。
在??/src/test/resources/application.yml??中,我們針對屬性的測試配置如下:
YAML
server
port0
spring
application
name order-service-test
cloud
service-registry
auto-registration
enabledfalse
loadbalancer
ribbon
enabledfalse
config
enabledfalse
jpa
show-sqltrue
eureka
client
enabledfalse
service-url
registerWithEurekafalse
okta
oauth2
issuer https //kmandalas/oauth2/default
location-service
url http //localhost 9999/v1/track/
在上述代碼段所示中,我們禁用spring.cloud.config、eureka.client和spring.cloud.service-registry.auto-registration的原因在于,方便孤立地測試微服務。因此,既不會有Spring Cloud Config服務器在啟動時,為OrderService的配置屬性提供服務;也不會有Eureka服務器提供注冊,并能夠使用它來按需調用FulfillmentService的動態服務發現。
數據庫
當出于測試目的而必須與數據庫(關系型或NoSQL)集成時,我們通常有如下三種選擇:
- 使用嵌入式或內存中(in-memory)方案,例如:H2,https://www.h2database.com/
- 使用一個能在測試期間可供訪問的真實數據庫
- 使用與生產數據庫接近甚至相同的臨時數據庫
不同的選項所涉及到的測試資源,將會不盡相同。
- 如果采用第一種方法,將H2進行集成和組件測試,那么由于生產環境的數據庫很可能與H2不同,因此您將不得不維護各種獨立的DDL和DML腳本。此外,您也可能會用到原生查詢、或其他特定于某個數據庫的功能。
- 如果您需要進行端到端或性能測試的話,那么就應該部署真實的數據庫,并在測試環境中啟動并運行它。對此,現代化的IaC(infrastructure as code,基礎設施即代碼)工具、以及詳盡的??測試數據管理??,將可以為項目按需提供靈活性。
- 在本測試示例中,我們將使用第三種方法,利用??testcontainers???和??Flyway??,實現與Spring Boot的配合,而數據庫才采用PostgreSQL。在testcontainers的幫助下,我們將在測試的初始化階段,創建一個臨時的dockerized數據庫實例。而Flyway將會在這個臨時模式(schema)上觸發???遷移腳本??(DDL/DML),以便我們的代碼將透明地、針對該臨時模式運行。而在測試完成時,我們會處理掉這個dockerized數據庫。
可見,我們實際上只需要OrderControllerTest類上的@Testcontainers注釋,以及如下的靜態聲明:
Java
static PostgreSQLContainer database = new PostgreSQLContainer("postgres:12")
.withDatabaseName("tutorial")
.withUsername("kmandalas")
.withPassword("dzone2022");
?
static void setDatasourceProperties(DynamicPropertyRegistry propertyRegistry) {
propertyRegistry.add("spring.datasource.url", database::getJdbcUrl);
propertyRegistry.add("spring.datasource.password", database::getPassword);
propertyRegistry.add("spring.datasource.username", database::getUsername);
}
內部服務調用
我們將使用??Spring Cloud OpenFeign??來調用FulfillmentService,它是另一個“內部”的Spring Cloud微服務,可以被注冊到Eureka上。在正常執行的情況下,后臺的feign客戶端能夠通過名稱定位目標服務實例,實現客戶端的負載均衡(如果發現了多個實例的話)。
在我們的測試中,在沒有Eureka(或者是??Consul??等其他發現機制)的情況下,我們需要通過如下兩個方面,盡可能真實地模擬此類集成:
- 通過??WireMock??啟動一個模擬服務器。該服務器能夠根據URL的不同模式,來截獲請求,并回復由我們提供的模擬響應。
- 使用@TestConfiguration來模擬各種FulfillmentService實例的發現,并將其指向WireMock服務器的URI。您可以通過鏈接--https://github.com/kmandalas/spring-cloud-component-tests/blob/50241126932fce3e9cfc6351291af5857f77806a/src/test/java/gr/kmandalas/dzone/OrderControllerTest.java#L55,查看到此類測試配置。
當然,您也可以使用??Hoverfly??作為嵌入式模擬服務器。在本示例里,我們通過如下依賴項設置,來引入WireMock:
XML
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
<scope>test</scope>
</dependency>
通過spring-cloud-starter-contract-stub-runner,WireMock在Spring Boot應用測試套件中的引導被簡化了許多,同時這對于??契約測試??(contract tests)也是非常實用的。請通過查看Spring Cloud Contract WireMock的鏈接--https://docs.spring.io/spring-cloud-contract/docs/current/reference/html/project-features.html#features-wiremock,了解更多相關信息。
有了上面的基礎,我們只需要使用@AutoConfigureWireMock去注釋測試類,并在測試資源目錄下的??JSON文件??中定義各種WireMock映射即可。
外部服務調用
在集成的過程中,為了能夠調用某些外部的(第三方)服務,我們仍然需要依賴有效的WireMock映射(畢竟能夠提供的響應多多益善),以便在??application.yml??中定義測試URL資源。下面是一個簡單的示例:
YAML
location-service
url http //localhost 9999/v1/track/
我們在外部服務的URL端點路徑處,提供了WireMock嵌入式服務器運行的主機和端口。端口號雖然不必經過硬編碼,但是可以被定義為動態的,以便在CI/CD管道中并行運行多個組件測試,且不會發生端口沖突。
值得一提的是,WireMock不僅可以用于模擬來自RESTful服務的各種JSON響應,還可以模擬基于SOAP的Web服務的響應。
安全
正如前文提到的,Spring Cloud微服務基礎設施通常能夠合并出一個諸如Spring Cloud Gateway的API網關。據此,我們可以使用OAuth 2.0、JavaScript對象簽名和加密(Object Signing and Encryption,JOSE)、以及JSON Web令牌標準的令牌中繼模式,來處理用戶的身份識別,授權應用程序查看他們的個人資料,以及訪問網關后面的安全資源。通常,此類安全設置會由如下組件構成:
- 單點登錄服務器,如??Keycloak???、Cloud Foundry 的用戶帳戶和???身份驗證服務器???、以及??諸如Okta??之類商用的OAuth2身份驗證提供程序。
- Spring Cloud Gateway之類的API網關服務器,將用戶帳戶的管理和授權委托給單點登錄服務器。
- 資源服務器:在本Spring Boot微服務示例中為OrderService。
針對本測試示例,我們在單獨測試Spring Boot微服務時,會采用??Spring Security??的SecurityMockMvcRequestPostProcessors。它將使我們能夠在MockMvc調用期間,傳遞有效的JWT,定義權限(即用戶角色),并在啟用安全性的情況下,測試組件的行為。例如:
Java
mockMvc.perform(get("/api/orders/11212/status").with(jwt())).andExpect(status().isOk())
和
mockMvc.perform(get("/api/orders/").with(jwt().authorities(new
SimpleGrantedAuthority("backoffice"))))
.andExpect(status().isOk());
小結
如今,對于成功的產品交付而言,開發人員是否能夠在CI/CD管道中,以自動化的方式執行各類測試是至關重要的。希望上述討論的有關Spring Cloud微服務組件測試的相關指南和注意事項,能夠給您的實際項目交付提供幫助。
譯者介紹
陳峻 (Julian Chen),51CTO社區編輯,具有十多年的IT項目實施經驗,善于對內外部資源與風險實施管控,專注傳播網絡與信息安全知識與經驗;持續以博文、專題和譯文等形式,分享前沿技術與新知;經常以線上、線下等方式,開展信息安全類培訓與授課。
原文標題:Component Tests for Spring Cloud Microservices,作者:Kyriakos Mandalas和Dimitris Stavroulakis