使用pact框架创建和验证消费者驱动合同的工具。
pactman的Python项目详细描述
包装工
pact模拟、生成和验证的python版本。
启用消费者驱动的合同测试,提供提供商服务的单元测试模拟 和用户项目的DSL,以及服务的交互回放和验证 提供程序项目。目前支持1.1、2和3版的pact规范。
了解更多关于什么是契约,以及它如何帮助您的信息 更有效地测试代码,查看pact文档
包含最初来自pact python项目的代码。
pactman由reecetech团队维护,作为其工具包的一部分 控制他们庞大(且不断增长)的微服务体系结构。
Pactman与Pact Python之比较
关键的区别在于所有的功能都是用python实现的,而不是用shell或forking 到ruby实现。这允许更好的模拟用户体验(它模拟urllib3 直接的),是更快,更少混乱的配置(多个提供者意味着产生多个ruby进程 在不同的端口上)。
其中pact python
需要管理后台ruby服务器,并手动启动和停止
它,pactman
允许更好的使用,比如:
importrequestsfrompactmanimportConsumer,Providerpact=Consumer('Consumer').has_pact_with(Provider('Provider'))deftest_interaction():pact.given("some data exists").upon_receiving("a request") \ .with_request("get","/",query={"foo":["bar"]}).will_respond_with(200)withpact:requests.get(pact.uri,params={"foo":["bar"]})
它还支持一组更广泛的PACT规范(1.1到3版)。
pact验证程序从一开始就被设计成与pact代理进行对话(两者都是为了发现pact 并返回验证结果。
还有其他一些生活质量的改善,但这些是最大的改善。
如何使用pactman
安装
pactman
需要运行python 3.6。
pip install pactman
签订协议
创建完整的合同需要两步:
- 在消费者方面创建一个测试,声明它对提供商的期望
- 创建一个提供程序状态,允许在对提供程序重播时传递合同 < > >
- 定义描述测试中的产品和服务的消费者和提供商对象
- 使用给定的
定义提供者的设置条件
用户a存在且不是管理员
- 定义消费者预期发出的请求将包含的内容
- 定义服务器应如何响应
- 当调用
has_pact_with()
时,默认情况下,它将删除 声明的消费者和提供商。 - 您可以调用
consumer('consumer')。在 你的测试。这可以作为pytest模块或会话fixture,或者通过其他一些 机制并将其存储在变量中。按照惯例,在我们的所有示例中,这称为
pact
。 - 如果不合适,您可以手动指示
与()有约定 保留(
file_write_mode="merge"
)或删除(file_write_mode="overwrite"
)现有的 协议文件。 - 如果没有
内容类型
头,假设json:serialise withjson.dumps()
,编码为 并添加头内容类型:application/json;charset=utf-8
- 如果有一个
content type
头,它显示application/json
然后用 dumps()并在头中使用字符集,默认为utf-8。 - 否则按原样传递
内容类型
头和正文。 不支持二进制数据。
< > > - 使用
pactman verifier
命令行程序,该程序根据 服务的运行实例,或 - 使用pactman内置的
pytest
支持将pact作为测试用例重播,允许 使用其他测试机制,如模拟和事务控制。 < > > - 在应用程序中具有在生产环境中不活动的端点,这些端点创建和删除数据存储状态
- 一个单独的应用程序,可以访问同一个数据存储来创建和删除, 就像一个单独的应用程序引擎模块或Docker容器指向相同的数据存储
- 一个独立的应用程序,可以用不同的数据存储状态启动和停止另一台服务器
- 对于具有providerstate的协定,
name
参数将是providerstate
值, 并且params
将为空。 - 对于具有providerstates的协议,函数将在
providerstates
中的每个条目中调用一次。 数组,其name
参数取自数组项name
参数,且params
来自参数
参数。 - 克隆存储库https://github.com/reecetech/pactman
- 从源代码或使用类似于pyenv的工具安装python 3.6
- 建议为项目创建一个python
< > >
要运行测试,请使用: 毒性
要打包应用程序,请运行:
python setup.py sdist
这将创建一个
dist/pactman-n.n.n.tar.gz
文件,其中ns是当前版本。 从那里可以使用pip安装它:pip安装/dist/pactman-n.n.n.tar.gz
发布历史记录
3.0.0(未来,弃用警告)
- 删除不推荐的
--pact使用者名称
命令行选项
2.25.0
- 添加允许pytest成功的选项,即使pact验证失败
2.24.0
- 在pytest中更好地集成pact故障信息
2.23.0
- 连接到协定代理时启用身份验证凭据设置
- 允许按使用者版本标记筛选从代理获取的协定
- 改进pytest命令行选项的命名和组织
2.22.0
- 更好地实现2.21.0中的更改
2.21.0
- 在命令行输出处理程序中处理警告级别消息
2.20.0
- 修复pytest模式以将数组元素规则失败正确检测为pytest失败
- 允许使用--pact consumer name将pytest验证运行限制为单个使用者
2.19.0
- 正确拆卸pact上下文管理器,其中pact用于多个
交互(
与交互1,交互2
而不是与pact
)。
2.18.0
- 更正导致urllib模拟中断的清理错误。
2.170
- 处理缺少任何提供程序状态(!)在Pytest设置中。
2.16.0
- 延迟检查pacts目录的恶作剧,直到实际写入pacts 允许模块级协议定义而无副作用。
2.15.0
- 修复头匹配规则的序列化结构。
- 将
"never"
添加到文件写入模式
选项。 - 处理x-www-form-urlencoded post请求正文。
2.14.0
- 改进冗长的信息以澄清他们所说的话。
2.13.0
- 添加在验证期间向提供程序提供附加头的功能(感谢@ryallsa)
2.12.1
- 修复pact python术语兼容性
2.12.0
- 添加
等于
和包括
pact v3+的匹配器 - 如果交互中缺少指定的头,则使验证失败
- 显著改进了对pytest提供者验证pact的支持
- 将协定状态调用失败转换为警告而不是错误
2.11.0
- 确保查询参数值是列表
2.10.0
- 允许
与()约定接受
文件写入模式
- 修复2.9.0中引入的错误,在该错误中生成多个pact将导致一个pact 正在录制
- 当使用dict查询(谢谢cong)调用时,用'u request'修复
。
。 - 使
start_mocking()
和stop_mocking()
对于非服务器mocking是可选的 - 添加快捷方式,使python-m pactman.verifier.command\u line只是
python-m pactman
(主要用于发布前的测试) - 处理
无
提供程序状态 - 确保用于生成pact文件的所有mock的pact规范版本一致
- 在模拟过程中关闭正文内容中的一些边缘案例,并在自述文件中记录
- 删除不推荐的
- 添加了
和@given()
作为为v3+协议定义附加提供程序状态的方法 - 为pact generation(serialization)添加了更多测试,修复了一些边缘大小写错误
- 修复了verifier中小写http方法的处理(谢谢cong!)
- 修复mocked
urlopen
未处理正确数量的位置参数的问题 - 修复在多个测试用例中由于未能检测到故障而导致的多个问题 (头、路径和数组元素规则可能尚未应用)
- 修复应用于数组中单个非第一元素的规则
- 修复<;v3协议中消费者/提供商名称的生成问题
- 修复空数组验证的一些错误
- 如果pact destination dir丢失且其父目录存在,则创建该目录
- 解决模拟请求查询和模拟验证问题
- 修复模拟验证中的头正则表达式匹配
- 实际使用传入的版本
- 修复一些pact v3版本的问题(感谢pan jacek)
- 恢复丢失的结果输出。
- 当请求中没有
正文
时,更正了请求有效负载的定义 - 发布到Broker时,正确确定PACT验证结果。
- 命令行错误处理中格式路径的正确使用。
- 为了清晰起见,对自述文件进行了调整。
- 将
pact verifier
命令重命名为pactman verifier
以避免 与提供命令行的其他现有包混淆 不兼容pact verifier
命令。 - 支持head请求的验证(oops)。
- 更正了项目元数据中的项目URL(感谢Jonathan Moss)
- 修复详细输出
- 添加了一些Trove分类器以帮助潜在用户。
- 更正了命名错误的命令行选项。
- 纠正了一些包装问题
- pactman的初始版本,包括reecetech的pact验证器版本3.17和pact python版本0.17.0
编写用户测试
如果我们有一个与外部服务通信的方法,我们将调用
提供者
和我们的产品,消费者
在
/users/<;user>;
获取有关特定用户的信息。
如果获取用户的代码如下所示:
importrequestsdefget_user(user_name):response=requests.get(f'http://service.example/users/{user_name}')returnresponse.json()
然后,消费者的契约测试可能看起来像这样:
importunittestfrompactmanimportConsumer,Providerpact=Consumer('Consumer').has_pact_with(Provider('Provider'))classGetUserInfoContract(unittest.TestCase):deftest_get_user(self):expected={'username':'UserA','id':123,'groups':['Editors']}pact.given('UserA exists and is not an administrator').upon_receiving('a request for UserA').with_request('GET','/users/UserA').will_respond_with(200,body=expected)withpact:result=get_user('UserA')self.assertEqual(result,expected)
这有几个重要的功能:
使用pact对象作为上下文管理器,我们调用测试中的方法 然后将与公约模拟通信。模拟将以 我们定义的项,允许我们断言该方法处理了响应和 返回预期值。
如果您希望在配置mock和验证交互时有更多的控制权,
使用分别设置和验证方法:
Consumer('Consumer').has_pact_with(Provider('Provider')).given('UserA exists and is not an administrator').upon_receiving('a request for UserA').with_request('GET','/users/UserA').will_respond_with(200,body=expected)pact.setup()try:# Some additional steps before running the code under testresult=get_user('UserA')# Some additional steps before verifying all interactions have occurredfinally:pact.verify()
关于契约关系定义的重要说明
您可能已经注意到,在我们的 示例:
pact=Consumer('Consumer').has_pact_with(Provider('Provider'))
这是因为每个测试套件只能执行一次。默认情况下,pact文件是 在定义该关系时清除,因此如果每次测试定义一次以上 套件您最终只会存储每个关系声明的最后一个协定。关于这个的更多信息 主题,请参见编写多个协议。
请求
当定义预期的http请求时 可以指定方法、路径、正文、标题和查询:
pact.with_request(method='GET',path='/api/v1/my-resources/',query={'search':'example'})
query
用于指定url查询参数,因此上面的示例需要
请求/api/v1/my resources/?搜索=示例
pact.with_request(method='POST',path='/api/v1/my-resources/123',body={'user_ids':[1,2,3]},headers={'Content-Type':'application/json'},)
您可以为预期的请求定义精确的值,如上面的示例所示, 或者可以使用后面定义的匹配器来帮助处理 变量。
一些重要的has_pact_with()options()
与(provider…)
特别值得一提的是:
version
声明提供程序支持的协议规范版本。默认为"2.0.0",但为"3.0.0"
如果您的提供商支持pact规范版本3,则也可以接受:
frompactmanimportConsumer,Providerpact=Consumer('Consumer').has_pact_with(Provider('Provider'),version='3.0.0')
文件写入模式
默认为"覆盖"
,应为该模式或"合并"
。覆盖确保
当调用has_pact_with()
时,将删除任何现有的pact文件。合并将保留
pact文件并将新的pact添加到该文件中。参见编写多个协议。
如果您绝对不希望写入pact文件,请使用"never"
使用模拟服务器
默认为false
并控制pactman
使用的模拟方法。默认为
patchurllib3
,它是支持请求的库,也被其他一些项目使用。如果你
正在使用另一个库来发出您的http请求,该请求在下面不使用
-使用它将http请求重定向到模拟服务器)。
还可以将urllib3
要将使用模拟服务器
参数设置为真
。这会导致pactman
运行实际的http服务器来模拟
请求(服务器正在监听pact.uripact\u use\u mocking\u server环境变量设置为"yes",以强制整个套件使用服务器
方法。您应该在测试之外声明pact参与者(消费者和提供者),并且需要
在测试之外启动和停止模拟服务。下面的代码显示了使用服务器可能
看起来像:
importrequestsfrompactmanimportConsumer,Providerpact=Consumer('Consumer').has_pact_with(Provider('Provider'))deftest_interaction():pact.given("some data exists").upon_receiving("a request") \ .with_request("get","/",query={"foo":["bar"]}).will_respond_with(200)withpact:requests.get(pact.uri,params={"foo":["bar"]})0
然后,您可以使用pact
来声明这些参与者之间的协议。
编写多个协议
在测试运行期间,您可能需要为消费者/提供者编写多个pact交互
关系。pactman
将按如下方式管理pact文件:
关于given()
使用given()
向提供程序指示它们应该具有一些状态,以便
能够满足互动。你应该同意这个州和它的规格
正在与提供商讨论。
如果您定义的是版本3协议,您可以更丰富地定义提供者状态,例如:
importrequestsfrompactmanimportConsumer,Providerpact=Consumer('Consumer').has_pact_with(Provider('Provider'))deftest_interaction():pact.given("some data exists").upon_receiving("a request") \ .with_request("get","/",query={"foo":["bar"]}).will_respond_with(200)withpact:requests.get(pact.uri,params={"foo":["bar"]})1
现在,您可以指定附加参数来伴随提供者状态文本。这些是
作为关键字参数传递,它们是可选的。您还可以提供其他提供商
使用和_given()
调用的状态,如果需要,可以多次调用。资讯科技与
given()
具有相同的调用约定:提供程序状态名和任何可选参数。
需要变量内容
如果用户信息总是 静态的,但是如果用户的最后一个更新字段设置为当前时间会发生什么 每次修改对象时?为了处理可变数据并使测试更加可靠, 有几个有用的匹配项:
包括(匹配器、样本数据)
3.0.0版+协议中提供
断言该值应包含给定的子字符串,例如:
importrequestsfrompactmanimportConsumer,Providerpact=Consumer('Consumer').has_pact_with(Provider('Provider'))deftest_interaction():pact.given("some data exists").upon_receiving("a request") \ .with_request("get","/",query={"foo":["bar"]}).will_respond_with(200)withpact:requests.get(pact.uri,params={"foo":["bar"]})2
matcher
和sample_data
由消费者和提供商分别使用,具体取决于
当它们在with_request()
或部分中使用时,将用()响应
协议的一部分。使用上述示例:
包括在请求中
当您为消费者运行测试时,mock将验证
使用者在其请求中使用的包含matcher
字符串,引发断言错误
如果无效。当供应商验证合同时,样本数据将
用于请求真正的提供商服务,在本例中为"垃圾邮件内容示例"
包括在响应中
当您为消费者运行测试时,mock将返回您提供的数据
如示例数据
,在本例中为示例垃圾邮件内容
。当合同在
提供者,将验证从真实提供者服务返回的数据以确保
包含匹配器字符串。
术语(匹配器,样本数据)
断言该值应与给定的正则表达式匹配。你可以用这个 在请求或响应中期望具有特定格式的时间戳,其中 你知道你需要一个特定的格式,但对确切的日期并不在意:
importrequestsfrompactmanimportConsumer,Providerpact=Consumer('Consumer').has_pact_with(Provider('Provider'))deftest_interaction():pact.given("some data exists").upon_receiving("a request") \ .with_request("get","/",query={"foo":["bar"]}).will_respond_with(200)withpact:requests.get(pact.uri,params={"foo":["bar"]})3
matcher
和sample_data
由消费者和提供商分别使用,具体取决于
当它们在with_request()
或部分中使用时,将用()响应
协议的一部分。使用上述示例:
请求中的术语
当您为消费者运行测试时,模拟将验证开始日期
消费者在其请求中使用的匹配器与断言错误匹配
如果无效。当供应商验证合同时,样本数据将
用于请求真正的提供者服务,在这种情况下1972-01-01
响应项
当您为消费者运行测试时,mock将返回您提供的
如示例数据
,在本例中为2016-12-15t20:16:01
。当合同在
provider,regex将用于搜索来自真实提供者服务的响应
如果正则表达式在响应中找到匹配项,则测试将被视为成功。
like(采样数据)
断言元素的类型与充足的数据。例如:
importrequestsfrompactmanimportConsumer,Providerpact=Consumer('Consumer').has_pact_with(Provider('Provider'))deftest_interaction():pact.given("some data exists").upon_receiving("a request") \ .with_request("get","/",query={"foo":["bar"]}).will_respond_with(200)withpact:requests.get(pact.uri,params={"foo":["bar"]})4
请求中的like
当您为使用者运行测试时,模拟将验证值是否 类型正确,如果无效则引发断言错误。当合同是 经提供者验证,将在对 真正的提供商服务。
响应类似
当您为消费者运行测试时,mock将返回示例数据
。
当在提供者上验证合同时,提供者生成的值
将检查服务以匹配示例数据的类型
将like应用于复杂的数据结构
当字典用作like的参数时,所有子对象(及其子对象等) 将根据其类型进行匹配,除非您使用更具体的匹配器(如术语)。
importrequestsfrompactmanimportConsumer,Providerpact=Consumer('Consumer').has_pact_with(Provider('Provider'))deftest_interaction():pact.given("some data exists").upon_receiving("a request") \ .with_request("get","/",query={"foo":["bar"]}).will_respond_with(200)withpact:requests.get(pact.uri,params={"foo":["bar"]})5
每个类(采样数据,最小值=1)
断言该值是由元素组成的数组类型
例如示例数据
。它可用于断言简单数组:
importrequestsfrompactmanimportConsumer,Providerpact=Consumer('Consumer').has_pact_with(Provider('Provider'))deftest_interaction():pact.given("some data exists").upon_receiving("a request") \ .with_request("get","/",query={"foo":["bar"]}).will_respond_with(200)withpact:requests.get(pact.uri,params={"foo":["bar"]})6
或者其他匹配器可以嵌套在内部以断言更复杂的对象:
importrequestsfrompactmanimportConsumer,Providerpact=Consumer('Consumer').has_pact_with(Provider('Provider'))deftest_interaction():pact.given("some data exists").upon_receiving("a request") \ .with_request("get","/",query={"foo":["bar"]}).will_respond_with(200)withpact:requests.get(pact.uri,params={"foo":["bar"]})7 < Buff行情>
注意,您不需要在 json响应,接收到的任何额外数据都将被忽略,测试仍将通过。
有关更多信息,请参见匹配
强制相等匹配等于
3.0.0版+协议中提供
如果有一个子项alike
需要与默认值一样的精确值匹配
有效性测试,然后可以使用等于
,例如::
importrequestsfrompactmanimportConsumer,Providerpact=Consumer('Consumer').has_pact_with(Provider('Provider'))deftest_interaction():pact.given("some data exists").upon_receiving("a request") \ .with_request("get","/",query={"foo":["bar"]}).will_respond_with(200)withpact:requests.get(pact.uri,params={"foo":["bar"]})8
车身有效载荷规则
假设body
负载是json数据。如果没有内容类型
标题
我们假设
内容类型:application/json;charset=utf-8
(json文本是unicode,而
默认编码为UTF-8)。
在验证过程中,将比较非json有效负载是否相等。
在模拟过程中,http响应将被处理为:
根据服务验证协议
有两个选项可用于根据您创建的服务验证协议:
使用Pactman验证器
运行pactman verifier-h
查看可用选项。运行在协议代理中注册到提供商的所有协议
importrequestsfrompactmanimportConsumer,Providerpact=Consumer('Consumer').has_pact_with(Provider('Provider'))deftest_interaction():pact.given("some data exists").upon_receiving("a request") \ .with_request("get","/",query={"foo":["bar"]}).will_respond_with(200)withpact:requests.get(pact.uri,params={"foo":["bar"]})9
您可以使用-l
传入本地协议文件,这将根据本地文件而不是代理来验证服务:
pip install pactman
0
您可以使用--custom provider header
传入要传递给provider state setup的头并验证调用。它可以
多次使用
pip install pactman
1
也可以在provider_extra_header
环境变量中提供额外的头,尽管命令
行参数将覆盖此项。
提供程序状态
在很多情况下,你的合同需要提供程序上存在的特定数据 成功通过。如果要获取用户配置文件,则该用户必须存在, 如果查询记录列表,则需要存在一个或多个记录。支持 将消费者和提供者的测试分离开来,pact提供了提供者的概念 表示与消费者通信提供商上应存在哪些数据。
在设置对提供者的测试时,还需要设置
这些提供者声明。pact验证程序通过向
您提供的<;提供程序设置url>;
。这个url可以是
在提供程序应用程序或单独的应用程序上。管理状态的一些策略包括:
有关提供程序状态的更多信息,请参阅提供程序状态上的协议文档。
要验证提供者的pact,您需要在提供者的测试套件中编写一个新的pytest测试模块。
如果您不想在通常的单元测试运行中使用它,您可以调用它 您的测试代码需要使用pactman提供的 您将需要在pytest中包含一些额外的命令行参数(如下所述)来指示
协议应来自何处,验证结果是否应公布给协议经纪人。 django项目的示例可能包含: 测试函数可以使用标准pytest fixture进行任何级别的模拟和数据设置-因此模拟
下游api或提供商内部的其他交互可以通过标准monkeypatching完成。 传递给pact验证器的 编写完pytest代码后,需要使用其他参数来调用pytest: 如果您从代理中提取了pacts并希望发布验证结果,请使用 例如: 如果需要查看导致协定失败的回溯,可以使用verbosity标志
到pytest( 有关所有命令行选项,请参阅pytest命令行帮助( 您还可以在环境变量 如果代理需要http basic auth,则可以在url中提供它: 或者在 如果您的经纪人需要不记名代币,那么您可以在命令行上提供或在
环境变量 如果您的消费者协议有标签(称为"消费者版本标签",因为它们附加到特定的
然后可以在命令行上指定要获取pact的标记。多个标记
可以指定,并且将验证与指定的任何标记匹配的所有pact。例如,确保
您正在根据消费者提供的协议版本验证提供商,请使用: 设置开发环境:使用
pytest验证协议
并调用git子模块update--initverify_pacts.py
pact_verifier
fixture,调用
它的verify()
方法提供服务运行实例的url(pytest django
提供
一个方便的live_服务器
fixture,在这里工作得很好)和一个回调函数来设置提供者状态(如所述
下面)
2
pip install pactman
提供者状态使用
pytest
provider_state
函数将传递给provider state
和
提供程序状态
用于正在验证的所有协议。控制的命令行选项
pytest
验证pact--pact broker url=<;url>;
提供pact代理的基本url,以便从
供应商。您还必须提供--pact provider name=<;providername>;
以标识要
从代理检索的协定。您可以提供--pact verify consumer=<;consumername>;
来限制
协议只对消费者进行了验证。与命令行验证程序一样,您可以提供
在代理URL中或通过pact_broker_auth
环境变量验证详细信息。如果你的经纪人
需要无记名代币,您可以提供使用--pact broker token=<;token>;
或pact\u broker\u token
环境变量。--pact files=<;文件模式>;
验证由通配符模式标识的一些磁盘上pact json文件
(Unix全局模式匹配)。--pact publish results
打开发布结果。此选项还要求您指定--pact provider version=<;version>;
3
pip install pactman
pytest-v
)。pytest-h
)中的"pact"部分。协议代理配置
pact\u broker\u url
中指定代理url。
4
pip install pactman
pact\u broker auth
环境变量中设置为user:password
pact\u broker\u token
按标记筛选代理协议
5
pip install pactman
开发
2.06
1.02.
1.0.8
1.0.7
1.0.6
1.0.5
1.0.4