使用pact框架创建和验证消费者驱动合同的工具。

pactman的Python项目详细描述


包装工

 src=

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

签订协议

创建完整的合同需要两步:

  1. 在消费者方面创建一个测试,声明它对提供商的期望
  2. 创建一个提供程序状态,允许在对提供程序重播时传递合同
  3. < > >

    编写用户测试

    如果我们有一个与外部服务通信的方法,我们将调用 提供者和我们的产品,消费者/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)

    这有几个重要的功能:

    • 定义描述测试中的产品和服务的消费者和提供商对象
    • 使用给定的定义提供者的设置条件用户a存在且不是管理员
    • 定义消费者预期发出的请求将包含的内容
    • 定义服务器应如何响应

    使用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请求,该请求在下面不使用urllib3 要将使用模拟服务器参数设置为。这会导致pactman运行实际的http服务器来模拟 请求(服务器正在监听pact.uri-使用它将http请求重定向到模拟服务器)。 还可以将pact\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文件:

    • 当调用has_pact_with()时,默认情况下,它将删除 声明的消费者和提供商。
    • 您可以调用consumer('consumer')。在 你的测试。这可以作为pytest模块或会话fixture,或者通过其他一些 机制并将其存储在变量中。按照惯例,在我们的所有示例中,这称为pact
    • 如果不合适,您可以手动指示与()有约定 保留(file_write_mode="merge")或删除(file_write_mode="overwrite")现有的 协议文件。

    关于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

    matchersample_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

    matchersample_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响应将被处理为:

    1. 如果没有内容类型头,假设json:serialise withjson.dumps(),编码为 并添加头内容类型:application/json;charset=utf-8
    2. 如果有一个content type头,它显示application/json然后用 dumps()并在头中使用字符集,默认为utf-8。
    3. 否则按原样传递内容类型头和正文。 不支持二进制数据。
    4. < > >

      根据服务验证协议

      有两个选项可用于根据您创建的服务验证协议:

      1. 使用pactman verifier命令行程序,该程序根据 服务的运行实例,或
      2. 使用pactman内置的pytest支持将pact作为测试用例重播,允许 使用其他测试机制,如模拟和事务控制。
      3. < > >

        使用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可以是 在提供程序应用程序或单独的应用程序上。管理状态的一些策略包括:

        • 在应用程序中具有在生产环境中不活动的端点,这些端点创建和删除数据存储状态
        • 一个单独的应用程序,可以访问同一个数据存储来创建和删除, 就像一个单独的应用程序引擎模块或Docker容器指向相同的数据存储
        • 一个独立的应用程序,可以用不同的数据存储状态启动和停止另一台服务器

        有关提供程序状态的更多信息,请参阅提供程序状态上的协议文档。

        使用pytest验证协议

        要验证提供者的pact,您需要在提供者的测试套件中编写一个新的pytest测试模块。 如果您不想在通常的单元测试运行中使用它,您可以调用它verify_pacts.py

        您的测试代码需要使用pactman提供的pact_verifierfixture,调用 它的verify()方法提供服务运行实例的url(pytest django提供 一个方便的live_服务器fixture,在这里工作得很好)和一个回调函数来设置提供者状态(如所述 下面)

        您将需要在pytest中包含一些额外的命令行参数(如下所述)来指示 协议应来自何处,验证结果是否应公布给协议经纪人。

        django项目的示例可能包含:

        pip install pactman
        
        2

        测试函数可以使用标准pytest fixture进行任何级别的模拟和数据设置-因此模拟 下游api或提供商内部的其他交互可以通过标准monkeypatching完成。

        提供者状态使用pytest

        传递给pact验证器的provider_state函数将传递给provider state提供程序状态用于正在验证的所有协议。

        • 对于具有providerstate的协定,name参数将是providerstate值, 并且params将为空。
        • 对于具有providerstates的协议,函数将在providerstates中的每个条目中调用一次。 数组,其name参数取自数组项name参数,且params来自 参数参数。

        控制的命令行选项pytest验证pact

        编写完pytest代码后,需要使用其他参数来调用pytest:

        --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全局模式匹配)。

        如果您从代理中提取了pacts并希望发布验证结果,请使用--pact publish results 打开发布结果。此选项还要求您指定--pact provider version=<;version>;

        例如:

        pip install pactman
        
        3

        如果需要查看导致协定失败的回溯,可以使用verbosity标志 到pytest(pytest-v)。

        有关所有命令行选项,请参阅pytest命令行帮助(pytest-h)中的"pact"部分。

        协议代理配置

        您还可以在环境变量pact\u broker\u url中指定代理url。

        如果代理需要http basic auth,则可以在url中提供它:

        pip install pactman
        
        4

        或者在pact\u broker auth环境变量中设置为user:password

        如果您的经纪人需要不记名代币,那么您可以在命令行上提供或在 环境变量pact\u broker\u token

        按标记筛选代理协议

        如果您的消费者协议有标签(称为"消费者版本标签",因为它们附加到特定的 然后可以在命令行上指定要获取pact的标记。多个标记 可以指定,并且将验证与指定的任何标记匹配的所有pact。例如,确保 您正在根据消费者提供的协议版本验证提供商,请使用:

        pip install pactman
        
        5

        开发

        请阅读contribution.md

        设置开发环境:

        1. 克隆存储库https://github.com/reecetech/pactman并调用git子模块update--init
        2. 从源代码或使用类似于pyenv的工具安装python 3.6
        3. 建议为项目创建一个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 正在录制
          <2.90.0/P>
          • 当使用dict查询(谢谢cong)调用时,用'u request'修复
          • 使start_mocking()stop_mocking()对于非服务器mocking是可选的
          • 添加快捷方式,使python-m pactman.verifier.command\u line只是python-m pactman (主要用于发布前的测试)
          • 处理提供程序状态
          • 确保用于生成pact文件的所有mock的pact规范版本一致
          <2.80.0/P>
          • 在模拟过程中关闭正文内容中的一些边缘案例,并在自述文件中记录
          <2.7.0

          • 添加了和@given()作为为v3+协议定义附加提供程序状态的方法
          • 为pact generation(serialization)添加了更多测试,修复了一些边缘大小写错误
          • 修复了verifier中小写http方法的处理(谢谢cong!)
          <2.61.1

          • 修复mockedurlopen未处理正确数量的位置参数的问题

          2.06

          • 修复在多个测试用例中由于未能检测到故障而导致的多个问题 (头、路径和数组元素规则可能尚未应用)
          • 修复应用于数组中单个非第一元素的规则
          • 修复<;v3协议中消费者/提供商名称的生成问题
          <2.5.0

          • 修复空数组验证的一些错误
          <2.4.0

          • 如果pact destination dir丢失且其父目录存在,则创建该目录
          <2.3.0

          • 解决模拟请求查询和模拟验证问题
          • 修复模拟验证中的头正则表达式匹配
          • 实际使用传入的版本
          • 修复一些pact v3版本的问题(感谢pan jacek)
          <2.2.0

          • 恢复丢失的结果输出。
          <2.1.0

          • 当请求中没有正文时,更正了请求有效负载的定义
          < >< > > >
          • 发布到Broker时,正确确定PACT验证结果。

          1.02.

          • 命令行错误处理中格式路径的正确使用。
          • 为了清晰起见,对自述文件进行了调整。
          <1.1.0

          • pact verifier命令重命名为pactman verifier以避免 与提供命令行的其他现有包混淆 不兼容pact verifier命令。
          • 支持head请求的验证(oops)。

          1.0.8

          • 更正了项目元数据中的项目URL(感谢Jonathan Moss)
          • 修复详细输出

          1.0.7

          • 添加了一些Trove分类器以帮助潜在用户。

          1.0.6

          • 更正了命名错误的命令行选项。

          1.0.5

          • 纠正了一些包装问题

          1.0.4

          • pactman的初始版本,包括reecetech的pact验证器版本3.17和pact python版本0.17.0

          欢迎加入QQ群-->: 979659372 Python中文网_新手群

          推荐PyPI第三方库


热门话题
JAVAutil。整数java的扫描器键盘输入   java通知运行后立即崩溃   java如何在一个只能由类修改而不能由其实例修改的类中生成静态变量?   数据库Java字段猜测   返回值周围的java括号为什么?   java Android更新通讯录中的联系人   一个消费者正在读取数据   java是否可以通过编程方式为蓝牙配对设置pin?   java Spring引导和buildResponseEntity()   java为什么序列化可以在没有实现可序列化的情况下工作   Java同步无助于相互排斥   twitter Java Twitter4J未在推文下显示源标签   为什么Javasocket不支持中断处理?