7 months ago

消費者驅動的契約測試(Consumer-Driven Contracts,簡稱CDC),是指從消費者業務實現的角度出發,驅動出契約,再基於契約,對提供者驗證的一種測試方式。

原本你要測試的話必須啟動相依的服務

透過 Spring Cloud Contract 的實踐之後你不用啟動這麼多服務,只需拿 Stub 來提供測試

簡單說就是先訂好消費者(consumer)要什麼樣的格式與範例資料,再基於這份契約開發生產者(producer),打包時,除了正常版本的應用程式,還會額外產生 Stub 的 jar ,這個 Stub 也是基於當初的契約來產生能夠透過 Http 回覆簡易資料的啟動器

producer 生產者

來看一下 生產者(producer) 的 pom.xml

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>customer-service</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>demo</name>
    <description>Demo project for Spring Boot</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.8.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <spring-cloud.version>Dalston.SR4</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-contract-verifier</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-contract-maven-plugin</artifactId>
                <version>1.1.4.RELEASE</version>
                <extensions>true</extensions>
                <configuration>
                    <baseClassForTests>com.example.demo.TestContract</baseClassForTests>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

比較要提的是要加入 spring-cloud-starter-contract-verifier 來幫你驗證是否符合契約部分
以及另外自己要加入 spring-cloud-contract-maven-plugin,baseClassForTests 這個就是你要符合契約的那支測試程式

看一下專案架構

首先來看一下所謂生產跟消費間的契約是什麼東西

shouldReturnAllCustomers.groovy
import org.springframework.cloud.contract.spec.Contract
import org.springframework.http.HttpHeaders
import org.springframework.http.MediaType

Contract.make {
    description "return all customers"

    request {
        url "/api/customers"
        method GET()
    }

    response {
        status 200
        headers {
            header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE)
        }
        body("data": [[id: 1L, name: "sam"], [id: 2L, name: "andy"]])
        //body("""{"data":[{"id":1,"name":"sam"},{"id":2,"name":"andy"}]}""")

    }
}

契約 採用 groovy 的 DSL 描述,所以非常好閱讀
可以得知 需要透過 /api/customers 取得一個 json 裡面 data 是一個 list

接下來我們就來實現這份契約

Customer.java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Customer {
    private Long id;
    private String name;
}
CustomerRepository.java
@Repository
public interface CustomerRepository extends JpaRepository<Customer, Long> {
}
Page.java
@Data
public class Page {
    private Collection<Customer> data;
}
CustomerRestController.java
@RestController
public class CustomerRestController {

    @Autowired
    private CustomerRepository customerRepository;

    @RequestMapping(path = "/api/customers")
    public Page getCustomers() {
        Page page = new Page();
        page.setData(customerRepository.findAll());
        return page;
    }
}

好了,基本上我們已經實踐契約的內容,接下來透過 verifier 驗證跟產生 Stub ,
也就是透過這支測試程式 com.example.demo.TestContract

TestContract.java
import com.jayway.restassured.module.mockmvc.RestAssuredMockMvc;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.Arrays;

@SpringBootTest(classes = DemoApplication.class)
@RunWith(SpringRunner.class)
public class TestContract {

    @Autowired
    private CustomerRestController customerRestController;
    @MockBean
    private CustomerRepository customerRepository;

    @Before
    public void before() {
        Mockito.when(customerRepository.findAll()).thenReturn(
                Arrays.asList(new Customer(1L, "sam"), new Customer(2L, "andy")));

        RestAssuredMockMvc.standaloneSetup(this.customerRestController);
    }
}

然後執行 maven clear install 測試完後就會裝在本機的 Maven 庫
也可以看到產生了兩個 jar
customer-service-0.0.1-SNAPSHOT.jar
customer-service-0.0.1-SNAPSHOT-stubs.jar

想測試 stub.jar 的話可以看 Stub Runner Boot
抓這個 https://dl.bintray.com/marcingrzejszczak/maven/stub-runner-boot-1.1.0.RELEASE.jar

啟動 stub

java -jar stub-runner-boot-1.1.0.RELEASE.jar --stubrunner.ids=com.example:customer-service:+:8866 --stubrunner.workOffline=true

之後你就可以從 http://localhost:8866/api/customers 取得跟契約一致的資料

consumer 消費者

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>customer-client</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>demo</name>
    <description>Demo project for Spring Boot</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.8.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <spring-cloud.version>Dalston.SR4</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-feign</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

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

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

要注意的只有記得引入 spring-cloud-starter-contract-stub-runner 依賴

測試時

CustcomerClientTest.java
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.client.RestTemplate;

import static org.junit.Assert.assertEquals;

@RunWith(SpringRunner.class)
@SpringBootTest(classes = DemoApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureStubRunner(ids = {"com.example:customer-service:+:8866"}, workOffline = true)
public class CustcomerClientTest {

    @Autowired
    private RestTemplate restTemplate;

    @Test
    public void testGetCustomers() {
        ParameterizedTypeReference<Page> ptf =
                new ParameterizedTypeReference<Page>() {
                };
        ResponseEntity<Page> responseEntity =
                restTemplate.exchange("http://localhost:8866/api/customers", HttpMethod.GET, null, ptf);
        assertEquals("size error!", 2, responseEntity.getBody().getData().size());
    }
}

只要在註解 AutoConfigureStubRunner ids 標記你要啟動那些服務的 stub,
格式是長這樣 groupId:artifactId:version:classifier:port

之後沒有遠端的服務也可以正常測試了

workOffline = true 是指使用本地 maven 庫,不要使用線上的版本,所以你只要把 Consumer 在你本機上 install 過就可以了

參考資料:
https://github.com/spring-cloud/spring-cloud-contract
http://cloud.spring.io/spring-cloud-static/Dalston.SR4/multi/multi_spring_cloud_contractstub_runner.html
https://spring.io/blog/2017/10/25/spring-tips-spring-cloud-contract-http
技术干货 | 消费者驱动契约(CDC) 之 SpringCloud Contracts
Marcin Grzejszczak访谈:Spring Cloud Contract

← 客製化 Swagger UI Springboot multipart max-file-size Exception →
 
comments powered by Disqus