场景
利用 How to integrate SEATA AT mode with Spring Cloud 中定义好的微服务,进行集成测试。
包含以下服务:
- 库存服务(storage)
- 用户账户服务(account)
- 订单服务(order)
- 业务服务(business)
业务服务作为直接被调用的接口,组织关联其他三个服务。下订单时,服务的调用顺序如下:
1. 减库存(storage)
2. 创建订单
2.1. 扣减用户账户(account)
2.2. 生成订单(order)
服务之间存在依赖关系,各服务对基础设施也有依赖(数据库、注册中心、分布式事务协调者等)。
集成测试包含两个测试用例:
- 业务服务成功调用:验证请求成功
- 业务服务调用出现异常:验证分布式事务生效,异常节点之前的服务数据已回滚
工具链
- Spring Boot:借助 Spring Boot 对测试框架的集成和支持,运行测试用例
- JUnit:集成测试用例编写
- Gradle:构建工具执行集成测试,自动化执行的基础
- docker:各服务运行环境在容器中运行,便捷搭建集成测试环境
- docker-compose:容器编排
- GitHub Actions:持续集成平台,负责自动执行集成测试
- wait-for-it:约束有依赖关系的服务进行启动等待
实战
编写集成测试用例
借助 Spring Boot 的支持(如 Testing with a running server),在随机端口启动一个 Spring Boot 应用,执行对业务服务的调用。
本例中基于集成测试基类(AbstractIntegrationTest.groovy),按场景中描述编写了两个测试用例,完整代码可参见 IntegrationTest.groovy:
@Test
void successRequest() {
vo.put('amount', 100)
post(url, JsonOutput.toJson(vo), HttpStatus.CREATED)
}
@Test
void failedRequest() {
def storage = resOfGet("http://localhost:8081/at/storage/$commodityCode", HttpStatus.OK).count
// 生成订单服务抛出异常
vo.put('amount', -10)
post(url, JsonOutput.toJson(vo), HttpStatus.INTERNAL_SERVER_ERROR)
// 应用全局事务,库存数据回滚,不会变更
def newStorage = resOfGet("http://localhost:8081/at/storage/$commodityCode", HttpStatus.OK).count
assert newStorage == storage
}
容器化部署所有服务
为简化集成测试运行环境,将所有服务运行在容器中,通过 Docker Compose 进行编排,部署在单节点。
因为服务之间存在依赖关系,被依赖的服务若未完成启动,可能会导致有依赖的服务也不能正常运行。为解决这个问题,仅依赖 Docker Compose 中提供的 depends_on 是不够的,因为 depends_on
仅能保证服务的启动顺序,不能保证服务启动完成(能正常提供服务)之后再启动后续服务。
官方文档中针对此问题也给出了 方案,可利用 wait-for-it
脚本强制服务进行等待。例如:
order:
image: propersoft/docker-jre:8u171-jre
volumes:
- ./modules/order/build/libs/order-0.0.1-SNAPSHOT.jar:/usr/local/demo/order.jar
- ./docker/wait-for-it.sh:/wait-for-it.sh
depends_on:
- mysql
- nacos
- storage
- account
- seata-server
command: ["./wait-for-it.sh", "-t", "0", "storage:8081", "--", "./wait-for-it.sh", "-t", "0", "account:8082", "--", "java", "-jar", "/usr/local/demo/order.jar"]
ports:
- "8083:8083"
完整的配置可参考 docker-compose。
这里可能还会遇到一个问题:Feign 接口首次调用失败。
服务之间通过 @FeignClient
进行 RESTful API 的调用,在默认配置下,Ribbon 是懒加载的,在首次请求时,才会开始初始化相关类,而这可能就会导致各服务启动完毕后,首次被 Feign Client 调用时,调用失败。
为解决这个问题,可以启用 Ribbon 的 eager load
,并配置相应客户端,如:
ribbon:
eager-load:
enabled: true
clients: account, storage
完整配置文件可参考 这里。
运行集成测试
在 docker-compose.yml 所在路径执行
$ docker-compose up -d
即可启动所有服务。因为服务间依赖及强制等待,所有服务启动完毕需要一段时间。
可访问最后一个服务( http://localhost:8084 ),或查看容器日志,确认服务是否完成启动。
之后可在源码中直接运行集成测试查看效果。
集成测试与单元测试分别执行
因为集成测试需要启动服务而单元测试不需要,且集成测试的执行时间一般都要比单元测试的时间长(主要消耗在环境准备上),故利用构建工具执行测试时,最好将单元测试与集成测试分开执行。
可将集成测试单独放到一个模块内,在常规测试任务中,将这个模块排除,并重新注册一个执行集成测试的任务,如:
├── account
├── business
├── integration-test
├── order
└── storage
test {
exclude '**/integration/**'
}
task integrationTest(type: Test, dependsOn: test) {
include '**/integration/**'
}
完整配置可见 integration-test.gradle。
之后可通过 gradlew test
执行单元测试,通过 gradlew integrationTest
执行集成测试。
单元测试中,涉及调用其他服务时,可通过注解
@MockBean
来模拟其他微服务的行为,例如 BusinessApplicationTest.groovy
在本地环境可验证集成测试执行结果为通过。
那么如何在持续集成环境自动化完成这个过程呢?
持续集成环境配置
完成以上步骤后,要完成自动化测试,理论上只剩下在 CI 上启动服务和执行集成测试任务了。
本例中选用的 CI 平台是 GitHub Actions,对于托管在 GitHub 上的项目,要使用 GitHub Actions,需要做的就是在代码仓库中增加配置文件,如 ./github/workflows/check.yml。
So easy 是吧,但在 GitHub Actions 中运行本例的集成测试时,依然遇到了两个问题:
- 无法映射到 3306 端口:不确定 GitHub Actions 环境中 3306 端口是不是给 mysql 的 action 预留了,docker 无法将容器端口映射到宿主的 3306 上。修改为其他端口即可。
- wait-for-it 无效:在 GitHub Actions 环境下 wait-for-it 脚本虽然能正常执行但没有起到实际作用,此处没有找到太好的解决办法,通过增加一个等待的 action,在执行
docker-compose up
后强行等待一段时间,等服务都启动完成后再去执行集成测试。具体例子可见 check.yml#L43-L46。
在 GitHub Actions 上的集成测试执行情况可见:https://github.com/AlphaHinex/seata-at-demo/actions?query=workflow%3ACheck
本文完整实例可见:https://github.com/AlphaHinex/seata-at-demo