Spring Cloud 接口契約測試

在微服務體系中,開發者要進行接口測試,一般有以下幾種方法:

1. 搭建完整的微服務環境,將所有依賴的微服務全部運行起來,然後針對要測試的微服務寫測試用例;

2. 使用 Mock 來模擬依賴的微服務以及數據庫的讀寫;

3. 契約測試,服務的提供者和消費者按照同樣的契約編寫自己的測試用例。

這其中,方法1的工作量比較大,維護這麼一個環境也是一個麻煩的事情,但是能真實模擬請求的完整流程;方法2能讓測試集中於自己的微服務中,但是一旦依賴的接口有變化,Mock並不能及時的反映出來,要到集成測試的時候纔可能發現,這是個隱患;方法3在微服務架構中是一個比較好的方法,服務的提供者和消費者同時按照同一個版本的契約進行各自獨立的開發和測試,又不用完整的運行整套微服務體系,在便捷性和準確性上都有一定的保證。

本文介紹在 Spring Cloud 微服務中,如何優雅的編寫接口測試用例,這其中依賴到了 Spring Cloud Contract(契約測試框架),DbUnit(數據庫工具,用來模擬數據庫的讀寫)。一個好的測試用例,應該在測試接口邏輯的完整性的條件下,不會對數據庫造成破壞(這就要使用DbUnit工具),運行測試用例時不會依賴其他的微服務(這就要使用契約測試)。

首先介紹下示例項目依賴的版本:

Spring Cloud:  Greenwich.RELEASE

DbUnit: 2.6.0

spring-test-dbunit-core: 5.2.0 (注意這個組件不能用 https://github.com/springtestdbunit/spring-test-dbunit 這裏面的,這個是比較舊的版本,已經無人維護,Spring boot 1.X 可以使用,Spring Boot2 就不行了,需要用  https://github.com/ppodgorsek/spring-test-dbunit 這個,這是對舊項目的 fork ,進行長期維護的版本)

具體的依賴還需要根據實際的 Spring Cloud 版本進行更換。

一、使用 DbUnit 完成對數據庫層面的Mock

DnUnit工具具體使用方法請自行百度,它的實現邏輯是根據你提供的數據庫連接信息,將對應的數據庫進行備份,然後將你準備的測試數據寫入到數據庫中,之後執行測試用例,所有測試用例執行完畢之後,再將備份信息還原到數據庫中,這樣就避免了對數據庫的破壞。

首先準備測試數據,在 src/test/resources 下面建立 testData.xml 文件,按照如下格式寫入測試數據

<?xml version='1.0' encoding='UTF-8'?>
<dataset>
    <user_ user_uuid="11111111" account="zhangsan" user_name="張三"/>
    <user_ user_uuid="22222222" account="lisi" user_name="李四"/>
</dataset>

假設我們有一個接口 http://localhost:8080/user/${userUuid} 根據 userUuid 獲取用戶信息,具體的實現不列出了,這不是這篇文章的重點,我們只要有這個接口存在就行,它會返回如下格式的json數據

{
	"errorCode": 0,
	"errorMsg": "SUCCESS",
	"data": {
		"userUuid": "11111111",
		"account": "zhangsan",
		"userName": "張三"
	}
}

然後編寫測試類:

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@Transactional(transactionManager = "transactionManager")
@Rollback(value = true)
@TestExecutionListeners({ 
    DependencyInjectionTestExecutionListener.class,
    DirtiesContextTestExecutionListener.class,
    TransactionDbUnitTestExecutionListener.class,
    DbUnitTestExecutionListener.class })
@DatabaseSetup("/testData.xml")
public class UserControllerTest {

    private static final Logger LOGGER = LoggerFactory.getLogger(UserControllerTest.class);

    private ObjectMapper mapper;
    @Autowired
    public MockMvc mvc;

    @Before
    public void setUp() {
        LOGGER.info("UserControllerTest init");
        RestAssuredMockMvc.mockMvc(mvc);
        
        this.mapper = new ObjectMapper();
    }

    @Test
    public void testCreateUser() throws Exception {
        this.mvc.perform(MockMvcRequestBuilders.get("/user/11111111")
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.jsonPath("errorCode").value(0))
                .andReturn();
    }
}

編譯運行,測試用例通過,可以查看下實際的數據庫是不是還是原來的狀態,如果是則表示 DbUnit 工具引入成功。當然這過程中編寫類似創建用戶的測試用例,更能看出來的 DbUnit 是否生效。

這中間過程中你可能會碰到一個問題: org.dbunit.database.AmbiguousTableNameException: EVALUATE ,這是一個很坑的問題,我在這個問題上糾結了兩天,各種百度 google 無果,最後發現是 Spring Cloud Greenwich.RELEASE 版本使用的 mysql-connector-java 是 8.0的版本,需要將其改成 5.X的版本才能使得 DbUnit 正常運行。

DbUnit 完美運行之後,接下來就是契約測試了。

二、Spring Cloud Contract

文檔:https://cloud.spring.io/spring-cloud-static/Greenwich.RELEASE/single/spring-cloud.html#_spring_cloud_contract

具體如何使用請自學。

先說下契約這個東西,對於服務提供者而言,契約可以用來約束其單元測試用例,服務提供者編寫的測試用例,必須符合這個契約,才能保證服務提供者提供的接口確實是符合這個契約的。對於服務消費者而言,契約可以模擬其調用這個微服務時,會得到什麼樣的結果。編寫契約可以使用 groovy 或者 yml,Spring Cloud Contract 可以根據這個契約生成 測試用例,我們可以有效利用這一點,簡化服務提供者的單元測試用例的編寫工作。

服務提供者:

引入依賴

<dependencies>
    ...
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-contract-verifier</artifactId>
        <scope>test</scope>
    </dependency>
    ...
</dependencies>
<build>
    <plugins>
        ...
        <plugin>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-contract-maven-plugin</artifactId>                
            <version>2.1.0.RELEASE</version>
            <!--Don't forget about this value !!--> 
            <extensions>true</extensions>
            <configuration>
                <!--MvcMockTest爲生成本地測試案例的基類-->  
                <baseClassForTests>com.walli.user.service.test.UserControllerTest</baseClassForTests>
            </configuration>
        </plugin>
        ...
    </plugins>
</build>

這裏說明下 baseClassForTests 這個屬性配置,這裏聲明 Spring Cloud Contract 自動生成測試用例的時候的基類,在這個基類中,你需要注入 MockMvc 的上下文(上面的測試類示例代碼中的@Before  RestAssuredMockMvc.mockMvc(mvc) 這一行)。

然後編寫契約,我採用的是 yml 方式, groovy 不是太熟,但是使用 groovy 肯定靈活性更高。

Spring Cloud Contract 默認會去 src/test/resources/contracts 目錄下去加載契約文件,這裏簡單一點我們就不改目錄了, 直接在這個目錄下創建契約文件 getUser.yml(契約文件的具體內容,還需要根據你實際的接口規則去編寫,此處返回的狀態等都只適合我的測試代碼,你可以組織各種各樣不同的參數提交來模擬各種複雜情況,以提高測試的代碼覆蓋率)

## 此文件爲 get user by userUuid 接口的契約

## 測試用戶不存在
request:
    method: GET
    url: /user/33333333
    headers:
        Content-Type: application/json
response:
    status: 500
    body:
        errorCode: 990004
    headers:
        Content-Type: application/json;charset=UTF-8
---
## 測試用戶正常獲取
request:
    method: GET
    url: /user/11111111
    headers:
        Content-Type: application/json
response:
    status: 200
    body:
        errorCode: 0
        errorMsg: SUCCESS
        data:
            userUuid: 11111111
            userName: 張三
            account: zhangsan
    headers:
        Content-Type: application/json;charset=UTF-8

契約編寫完成之後,直接 mvn clean install 編譯,如果成功,你可以在 代碼目錄的 target 目錄下看到一個叫做 XXXX-stub.jar 的文件,這個 stub 文件就是你可以交給服務消費者使用的文件,你可以把它放到你們自己的 maven 倉庫中,供別人下載。

然後,你可以在 target\generated-test-sources 找到一個 ContractVerifierTest 的類,它 extends 你寫的 UserControllerTest 類,這裏面,就是根據契約自動生成的測試用例。

服務消費方:

服務消費方關鍵就是要引入服務提供方給出的 stub 文件,有遠程和本地兩種引入方式。

首先需要引入依賴:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
    <scope>test</scope>
</dependency>

本地引入 stub 時,需要先獲取 服務提供方的代碼然後編譯完成,即保證本地的 maven 倉庫中有對應的 stub 文件。

然後編寫消費方的測試代碼:

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureStubRunner(ids = {"com.walli:cloud-user-service-server:1.0.1:stubs:9900"},
		stubsMode = StubRunnerProperties.StubsMode.LOCAL)
public class SsoControllerTest {

    private static final Logger LOGGER = LoggerFactory.getLogger(SsoControllerTest.class);

    private ObjectMapper mapper;
    @Autowired
    public MockMvc mvc;
    
    @Before
    public void setUp() {
        LOGGER.info("SsoControllerTest init");
        
        this.mapper = new ObjectMapper();
    }
    
    @Test
    public void testLogin() throws Exception {
        this.mvc.perform(MockMvcRequestBuilders.get("/user/111111")
                .contentType(MediaType.APPLICATION_JSON)
                .content(this.mapper.writeValueAsString(param))
                .accept(MediaType.APPLICATION_JSON))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.jsonPath("errorCode").value(0))
                .andReturn();
    }
}

其中 

@AutoConfigureStubRunner(ids = {"com.walli:cloud-user-service-server:1.0.1:stubs:9900"},
		stubsMode = StubRunnerProperties.StubsMode.LOCAL)

這一段即爲本地使用 stub 文件,如果是遠程調用,需要按照如下方式進行:

首先在 application.yml 文件中聲明stub依賴方式:

stubrunner:
  ids: 'com.walli:cloud-user-service-server:1.0.1:stubs:9900'
  repositoryRoot: http://repo.spring.io/libs-snapshot

repositoryRoot 改成自己的,然後把測試代碼上的 @AutoConfigureStubRunner 中StubsMode 改成 REMOTE即可。

編譯運行測試用例通過,即表示消費方契約測試成功,因爲你並沒有啓動 cloud-user-service-server,但是你的測試用例還是通過了,並且調用的接口返回值是契約中約定的值。

 

關鍵概念&知識點

1. priority 數值越小,優先級越高

    在契約定義中,我們有時候不僅要定義接口的正常返回值,也有可能要定義異常的返回值(即消費者按照此規則傳遞參數之後獲得的是一個錯誤),這種情況我們可以利用 priority,將錯誤返回值的契約設置較高優先級,將正常返回值的契約優先級降低,這樣有利於消費者需求錯誤響應時能拿到相應的錯誤,而不是匹配上正確的響應結果。

2. request 中的一些概念

    request 中的參數,不管是 queryParameters 還是 body 中的參數,如果你不寫相應的 matcher 的話,契約中就是默認生成參數必須嚴格相等的契約。

    request.matchers 是用來定義請求參數的規則的,即消費者必須按照此規則來提交參數;並且,matchers 中最好只定義必傳參數的規則,否則就會面臨不必傳的參數,消費者使用契約時必須填寫該參數纔行;契約中不存在的參數,默認都是可傳可不傳的。

    request matchers queryParameters 的 type 可用如下值:
    equal_to_json, equal_to, not_matching, matching, containing, absent, equal_to_xml

3. response 中的一些概念

    response.body 的返回值,是用來模擬給消費者的返回值的,同時也用來校驗spring cloud contract 自動生成測試用例的返回結果是不是跟這個值匹配

    response.matchers 的用處是當 spring cloud contract 自動生成測試用例得到的返回結果與 response.body 中定義的值不一樣時,比如創建用戶生成 uuid,這個 uuid 必然是隨機的,response.body 必然無法自定義,所以這時候需要寫 response.matchers 定義 uuid 規則,只要實際的返回值符合這個規則,測試用例就認爲可以通過。matchers 不特殊定義匹配規則的字段,就是嚴格等於 response.body 中定義的值

    response.body 中最好寫調用此接口必然會返回的值,同時 response.matchers 中需要對應 response.body 返回的字段有動態值的字段寫好相應的 matcher,否則自動生成的測試用例無法通過

 

侷限性

1. 在類似 新增/更新 這種參數不確定的請求中,request 的參數只能寫必傳值,response 也只能寫必然會返回的數據(更新可以所有有效字段)。

2. reuqest.queryParameters 無法編寫數組類型的參數契約,只能默認全都允許(即 request matchers 中判斷其不爲空即可)

3. request.body 中的參數對象如果每個字段都可爲空,這種契約是沒法編寫的,契約的 request.body 不寫的話代碼會報 body 不能爲空的錯誤,寫的話又強制了定下契約body的字段必須傳遞,這是個矛盾點,所以只能挑一個絕大多數情況下都會傳遞的參數寫到 request.body 裏面,消費者調用的時候傳遞一下這個參數。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章