在验收测试阶段,基于微服务架构的应用相对于单体架构的应用而言,具有以下挑战:
-
复数的服务增加了测试环境搭建的难度
-
各种异常情况的模拟变得困难,基于 Mock 的测试方式无法对整个调用链路作异常模拟,进而使得对整体架构的健壮性测试变得困难
-
基于成本和效率的原因,测试工作已经不适合通过人工完成
我们认为对基于微服务架构的应用,其验收测试应该具有以下特征:
-
自动化、可重复、易于集成CI工具
-
能够在测试运行时修改服务的行为
下面我们通过一个 Demo 来介绍如何利用 Docker、Cucumber、Byteman、Fabric8 docker-maven-plugin、Spotify dockerfile-maven-plugin达成以上目标。
01 Demo 实践
一共有两个服务:
- Product Service(商品服务)
- Product Price Service(商品价格服务)
Product Service 提供了一个查询接口用于获得商品信息及其价格信息的组合结果,这相当于跨服务的 SQL JOIN。
Product 的 Schema:
{
"id": "<string>",
"name": "<string>",
"description": "<string>"
}
ProductPrice的Schema:
{
"id": "<string>",
"price": "<number>"
}
Product Service返回的Schema则是:
{
"products": [
{
"id": "<string>",
"name": "<string>",
"description": "<string>",
"price": <number>
}
]
}
该接口的实现逻辑是:
-
Product Service 本地查询得到 Product List
-
Product Service 调用 Product Price Service 接口得到 ProductPrice List
-
拼装结果
此外还有一个要求,当 Product Price Service 出现异常时,Product Service 依然要能够返回结果,只不过 price 字段为 null,即无论如何 Product Service 都要能够返回结果。
02 实现步骤
构建 Docker Image
为了能够便利地搭建测试环境,我们需要先为Product Servcie和Product Price Service构建 Docker Image。利用 Spotify dockerfile-maven-plugin 可以很方便地做到这一点,它没有引入额外的概念,只要你会写 Dockerfile 就行。我们在 Product Service 和 Producer Price Service 的 pom.xml 中添加类似以下的配置:
<plugin>
<groupId>com.spotify</groupId>
<artifactId>dockerfile-maven-plugin</artifactId>
<version>${dockerfile-maven-plugin.version}</version>
<configuration>
<repository>msat-${project.artifactId}</repository>
<tag>${project.version}</tag>
<buildArgs>
<JAR_FILE>${project.build.finalName}-exec.${project.packaging}</JAR_FILE>
</buildArgs>
</configuration>
<executions>
<execution>
<id>build</id>
<phase>package</phase>
<goals>
<goal>build</goal>
</goals>
</execution>
</executions>
</plugin>
并且提供了 Dockerfile:
FROM openjdk:8-jre-alpine
ARG JAR_FILE
ENV JAR_FILE=${JAR_FILE}
RUN mkdir /maven
COPY target/${JAR_FILE} /maven
COPY target/lib/byteman.jar /maven
ENTRYPOINT java $JAVA_OPTS -jar /maven/$JAR_FILE
EXPOSE 8080
注意我们在 Image 中添加了 byteman.jar,利用它我们可以在运行时修改程序。
编写验收测试脚本
我们新建一个验收测试的Maven项目,然后使用 Cucumber 编写了以下两个场景的验收测试脚本:
正常情况:
Feature: List product information with price
Scenario: Everything is good
Given Product Service is up and running
And Product Price Service is up and running
When User query product list
Then Get following products
| id | name | description | price |
| animal-1 | dog | woof woof | 1000 |
| animal-2 | duck | quack quack | 40 |
| animal-3 | fox | what does the fox say? | 5000 |
这个脚本的大致意思是在 Product Service 和 Product Price Service 都启动的情况下,当用户查询 Product 信息时,我们会得到上述表格中的结果。
Product Price Service 异常情况:
Feature: List product information with price
Scenario: Product Price Service throws exception when being queried
Given Product Service is up and running
And Product Price Service is up and running
Given Install the byteman script product_price_exception.btm to Product Price Service
When User query product list
Then Get following products
| id | name | description | price |
| animal-1 | dog | woof woof | |
| animal-2 | duck | quack quack | |
| animal-3 | fox | what does the fox say? | |
我们在这里使用了 Byteman 给 Product Price Service 注入了异常情况:
Given Install the byteman script product_price_exception.btm to Product Price Service
product_price_exception.btm 的内容是这样的:
RULE throw exception
CLASS me.chanjar.msat.productprice.FakeProductPriceRepository
METHOD listAll
AT ENTRY
IF TRUE
DO debug("throw RuntimeException here"),
throw new RuntimeException("Product Repository Error!")
ENDRULE
意思是在调用 FakeProductPriceRepository.listAll 方法时抛出异常,注意这样做并没有修改 Product Price Service 的源码,而是在运行时修改了它的逻辑。
接下来我们为上面的验收测试脚本实现逻辑(下面代码与实际上有所不同,这是为了尽量使得代码篇幅精简):
public class Stepdefs {
private List<Map<String, String>> answer;
@Given("^Product Service is up and running$")
public void productServiceIsUpAndRunning() {
probe("Product Service", PRODUCT_ADDRESS);
}
@And("^Product Price Service is up and running$")
public void productPriceServiceIsUpAndRunning() {
probe("Product Price Service", PRODUCT_PRICE_ADDRESS);
clearBytemanScript();
}
@When("^User query product list$")
public void queryProductList() {
answer = given()
.when()
.get(PRODUCT_ADDRESS + "/products")
.then()
.statusCode(is(200))
.extract()
.body()
.jsonPath()
.getList("products", Map.class);
}
@Given("^Install the byteman script ([A-Za-z0-9_\\.]+) to Product Price Service$")
public void injectExceptionIntoProductPriceService(String bytemanScript) throws Exception {
injectBytemanScript("target/test-classes/" + bytemanScript);
}
@Then("^Get following products$")
public void compareResult(List<Map<String, String>> expected) {
assertThat(answer).containsExactlyInAnyOrderElementsOf(expected);
}
}
关于 Cucumber 和 Byteman 的更详细的介绍可以见 ServiceComb Saga 使用 Cucumber 做验收测试源码分析。
自动化搭建测试环境
我们希望能够在 Maven 的 integration-test 阶段搭建测试环境、执行上述验收测试脚本。在 pom.xml 中添加到 Fabric8 docker-maven-plugin:
<plugin>
<groupId>io.fabric8</groupId>
<artifactId>docker-maven-plugin</artifactId>
<configuration>
<showLogs>true</showLogs>
<images>
<image>
<name>msat-product:${project.version}</name>
<alias>msat-product</alias>
<run>
<wait>
<log>Started [a-zA-Z]+ in [0-9.]+ seconds</log>
<time>120000</time>
</wait>
<links>
<link>msat-product-price:msat-product-price</link>
</links>
<ports>
<port>product.port:8080</port>
</ports>
</run>
</image>
<image>
<name>msat-product-price:${project.version}</name>
<alias>msat-product-price</alias>
<run>
<env>
<JAVA_OPTS>
-Dorg.jboss.byteman.debug=true -Dorg.jboss.byteman.verbose=true
-javaagent:/maven/byteman.jar=port:9091,address:0.0.0.0,listener:true
</JAVA_OPTS>
</env>
<wait>
<log>Started [a-zA-Z]+ in [0-9.]+ seconds</log>
<time>120000</time>
</wait>
<ports>
<port>product-price.port:8080</port>
<port>product-price.byteman.port:9091</port>
</ports>
</run>
</image>
</images>
</configuration>
<executions>
<execution>
<id>start</id>
<phase>pre-integration-test</phase>
<goals>
<goal>start</goal>
</goals>
</execution>
<execution>
<id>stop</id>
<phase>post-integration-test</phase>
<goals>
<goal>stop</goal>
</goals>
</execution>
</executions>
</plugin>
这样一来就能够在 pre-integration-test 阶段启动容器,也能在 post-integration-test 阶段销毁容器了。
Take a run
接下来只需要 mvn clean install 它就会:
-
构建:
-
构建 Product Service 项目,并为其构建 Docker Image
-
构建 Product Price Service 项目,并为其构建 Docker Image
-
-
验收测试:
-
启动 Product Service 和 Product Price Service 的容器
-
执行验收测试脚本
-
销毁上述创建的容器
-
如果你想自己试试可以下载本项目源码。
欢迎开发者朋友们加入 ServiceComb 社区,一起做些有意思的事情。加入Servicecomb社区