原文链接
https://codefresh.io/docker-tutorial/how-to-test-microservice-integration-with-pact/
挑战:微服务集成测试
迁移到微服务对测试我们的系统产生了新的挑战。理论上每个微服务都应该是隔离的并可以独立操作。但在实践中一个服务如果没有其他部分通常没什么用。另一方面 - 为一个服务拉起整个系统的拓扑进行测试抵消了微服务期望带来的模块化和封装。
挑战在于如何检验与其他服务集成后没有问题。我们希望越早越好。而且我们不想将复杂的生产环境重现一遍。一般来说这种检验是集成功能测试或叫端到端测试。但实际是当我们的系统越来越复杂 - 端到端带来的收益越少。 大量的相互依赖导致误报和很长的执行周期。 使得测试变得很难管理与调试。
这甚至有一个测试金字塔理论(最初由Mike Cohn在他的著作‘Succeeding with Agile’中提到)讲述了为了优化你的投入,你需要更少的高层次的端到端测试,写更多的低层次的单元测试。
请阅读本文并看看Codefresh(https://codefresh.io/codefresh-signup/?utm_source=Blog&utm_medium=Post&utm_campaign=pactT), 他是对于Docker最好的CI。
单元测试很好!但在它带来的所有收益中 - 他们对测试与其他服务的集成没什么作用。
那我们怎样保证每个服务团队可以独立的迭代但又能保证整体系统的健康呢?我们如何实现持续交付,小批量生产,快速反馈,而又不会在每次变更时引起服务出问题呢?
一个可能的答案是Consumer-Driven Contract(CDC) 测试。这种测试策略是基于一种多年前就定义的服务进化模式。它现在分布式系统变得更常见后变得更适合了。
Consumer-Driven Contracts:
我尝试简单解释一下。 Consumer-Driven Contracts实际就是面向服务与服务关系的合约。意思就是不想以前是provider提供方定义接口与服务级别是什么样(同事消费者consumer尽量适配) - 现在消费者来领舞。 每个消费者来定义它期望服务提供方需要交付与需要检查的。这就将集成的责任转移到服务提供方。
那就变成以下流程:
在商务合约上者通常描述成‘将消费者放在第一位’ 或‘倾听你的客户’。因为想要提供最好的服务我们需要尽量做到客户期望和需要的。而不是我们假设对的事。
当讨论微服务进化时 - 在那种每个服务都有一个独立团队开发的大型企业里尤其重要。有时这些团队也可能在不同的地理位置和区域。这影响了即时沟通和让业务功能进化更有挑战性。
合约测试框架
消费者驱动合约当然可以通过投资团队间的沟通与协作来管理。 也可以通过使用结构化的系列化格式如protobuf,thrift或messagepack消息体来解决。但如果要管理一个定义好的流程 - 最好使用框架,尤其如果是个开源的。
这种框架已经出现了。这其中最杰出和活跃的是Pact和Spring Cloud Contract。后者只针对使用JVM的项目。 而Pact使用Ruby写的但可以支持很多语言,包括Java,Go,Python,Javascript。 让它很适合在复杂,多样性的微服务系统中使用。
今天我们会看看如何在两个服务间定义和校验合约。消费者服务是用Python写的。而提供方服务是用Go写的。测试会在我们的CI/CD流程中进行 - 也就是在Codefresh流水线里面。
Pact
所以,Pact怎么工作的?它开始于消费者。
消费者服务的开发写一个测试。测试定义了与提供方的集成。这包括了提供方需要的状态,请求的消息体和期望的结果。基于这个定义Pact建立和运行一个提供方的桩来进行测试。这个测试的输出回事一个或多个json文件,一般是这样的:
{
"consumer": {
"name": "billy"
},
"provider": {
"name": "bobby"
},
"interactions": [
{
"description": "My test",
"providerState": "User billy exists",
"request": {
"method": "POST",
"path": "/users/login",
"headers": {
"Content-Type": "application/json",
},
"body": {
"username":"billy",
"password":"issilly"
}
},
"response": {
"status": 200,
}
},
],
"metadata": {
"pactSpecification": {
"version": "2.0.0"
}
}
}
这就是合约,这就是pact。 现在他们被传给服务提供方。也可以被提交给共享的git仓库,或通过Pact Broker应用上传到共享的文件存储。
一旦合约更新过了 - 提供方需要对其进行测试验证是否仍符合要求。它通过使用共享的pact文件运行它自己的校验测试而不是真实版本的服务。如果所有的交互是符合预期的并测试通过了 - 我们就可以继续了。 如果不 - 提供方的开发需要通知消费方的开发。然后,他们可以一起分析什么导致了合约的失败。
我们的例子:
我们会测试2个小服务的集成。
服务provider是我们在jenkins plugin例子里使用过的相同的服务。它叫’bringon‘,是用Go写的一个是用mongoDB的保存软件构建信息的注册表。
我们的consumer是一个很薄的python客户端,它只知道从bringon是用构建编号来获取构建信息的。
在CDC里consumer先开始 - 我们从这来。
consumer代码现在由一个带有2个函数的client.py文件组成。我们只关注叫’build‘的函数 - 它是我们要测试的函数。
import requests
…
def getbuild(host, port, buildnum):
"""Fetch a build by number ."""
uri = 'http://' + host + ':' + port + '/builds/' + str(buildnum)
return requests.get(uri).json()
为了为它生成pact - 我们写一个叫build_test.py的测试文件:
import atexit
import unittest
import client
from pact import Consumer, Provider
pact = Consumer('buildReader').has_pact_with(Provider('bringon'))
pact.start_service()
atexit.register(pact.stop_service)
class GetBuildInfoContract(unittest.TestCase):
def test_get_build(self):
true = True
expected = {
u'name':u'#3455',
u'completed': true, #boolean
u'info':{
u'coverage':30,
u'apiversion':0.1,
u'swaggerlink':u'http://swagger',
u'buildtime':230}
}
(pact
.given('build 3455 exists')
.upon_receiving('a request for build 3455')
.with_request('get', '/builds/3455')
.will_respond_with(200, body=expected))
with pact:
result = client.build(3455)
self.assertEqual(result, expected)
这很直接 - 我们建了一个mock的service,定义了一个期望的http reponse和body, 并调用client.build()来保证交互是按期望进行的。
如果一切正常 - 一个叫buildreader-bringon.json的pact文件会写入到我们的工作目录。
现在我们可以将这个文件发给bringon的开发,让他们可以用这个pact来测试他们的服务。
这可以用pact-go来完成 - Golang的框架。测试看起来会是这样:
func TestPact(t *testing.T) {
go startInstrumentedBringon()
pact := createPact()
// Verify the Provider with local Pact Files
log.Println("Start verify ", []string{filepath.ToSlash(fmt.Sprintf("%s/buildreader-bringon.json", pactDir))},
fmt.Sprintf("http://localhost:%d/setup", port), fmt.Sprintf("http://localhost:%d", port), fmt.Sprintf("%s/buildReader-bringon.json", pactDir))
err := pact.VerifyProvider(types.VerifyRequest{
ProviderBaseURL: fmt.Sprintf("http://localhost:%d", port),
PactURLs: []string{filepath.ToSlash(fmt.Sprintf("%s/buildReader-bringon.json", pactDir))},
ProviderStatesSetupURL: fmt.Sprintf("http://localhost:%d/setup", port),
})
if err != nil {
t.Fatal("Error:", err)
}
}
记住这需要一点额外的工作。我们需要需要实现startInstrumentedBringon()方法,它使用额外的'/setup' endpoint来定义服务状态并启动我们的服务。在我们的场景这用于创建一个入口点来满足我们消费者的期望。我们也需要创建一个Pact客户端对象来校验所有交互动作。像这样:
func createPact() dsl.Pact {
// Create Pact connecting to local Daemon
log.Println("Creating pact")
return dsl.Pact{
Consumer: "buildreader",
Provider: "bringon",
LogDir: logDir,
PactDir: pactDir,
}
}
一个使用pact-go的缺点是需要你在后端运行一个常驻进程。这个进程控制服务的初始化,关闭和pact校验。
这在容器内运行一个独立临时进程不太合适。
所以如果我们需要的是用pact测试我们的服务 - 我们可以使用pact-go包中的轻量级pact-provider-verifier工具。
像这样:
pact-provider-verifier --pact-urls <path_to>/buildreader-bringon.json --provider-base-url http://localhost:8091 --provider-states-setup-url http://localhost:8091/setup
请记住在这个例子里我们需要实现和构建'/setup' endpoint作为我们服务的一部分。这在我们想让我们的服务可测试时是个好主意。
服务的代码可以在我们的Github找到:
bringon(也就是Provider):
https://github.com/codefreshdemo/bringon
buildreader(也就是消费者)
https://github.com/antweiss/cdc-pact-demo
Pact源码和例子:
https://github.com/pact-foundation
在这个博客的下篇我们会展示如何让运行合约测试作为你的Codefresh(译者注:codefresh.io是一个CI/CD提供商) 流水线的一部分。