跳转到主要内容

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

项目描述

pactman

Pact 模拟、生成和验证的 Python 版本。

启用 消费者驱动合同测试,为提供者服务提供单元测试模拟和消费者项目的 DSL,以及为服务提供者项目提供交互回放和验证。目前支持 Pact 规范 的 1.1、2 和 3 版本。

要了解 Pact 是什么以及它如何帮助您更有效地测试代码,请参阅 Pact 文档

包含原始来自 pact-python 项目的代码。

pactman 由 ReeceTech 团队维护,作为他们工具集的一部分,以控制其庞大的(且不断增长)的微服务架构。

pactman 与 pact-python 的比较

关键区别是所有功能都在 Python 中实现,而不是通过外壳调用或分支到 Ruby 实现。这允许有更愉快的模拟用户体验(它直接模拟 urllib3),更快,更少的混乱配置(多个提供者意味着在多个端口上生成多个 Ruby 进程)。

pact-python 需要管理后台 Ruby 服务器并手动启动和停止它的情况下,pactman 允许更优雅的使用方式:

import requests
from pactman import Consumer, Provider

pact = Consumer('Consumer').has_pact_with(Provider('Provider'))

def test_interaction():
    pact.given("some data exists").upon_receiving("a request") \
        .with_request("get", "/", query={"foo": ["bar"]}).will_respond_with(200)
    with pact:
        requests.get(pact.uri, params={"foo": ["bar"]})

它还支持更广泛的 pact 规范(版本 1.1 到 3)。

从一开始就设计 pact 验证器与 pact 代理通信(用于发现 pact 并返回验证结果)。

还有一些其他的生活质量提升,但这些都是最大的。

如何使用 pactman

安装

pactman 需要 Python 3.6 运行。

pip install pactman

编写 Pact

创建完整契约是两步过程

  1. 在消费者端创建一个单元测试,声明对提供者的期望
  2. 创建一个提供者状态,允许契约在重放时通过提供者

编写消费者测试

如果我们有一个与我们的外部服务之一通信的方法,我们将它称为 Provider,我们的产品 Consumer 正在击打 Provider 上的 /users/<user> 端点以获取特定用户的信息。

如果 Consumer 获取用户的代码看起来像这样

import requests

def get_user(user_name):
    response = requests.get(f'http://service.example/users/{user_name}')
    return response.json()

那么 Consumer 的契约测试是一个常规单元测试,但 使用 pactman 模拟,可能看起来像这样

import unittest
from pactman import Consumer, Provider

pact = Consumer('Consumer').has_pact_with(Provider('Provider'))

class GetUserInfoContract(unittest.TestCase):
  def test_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)

    with pact:
      result = get_user('UserA')

    self.assertEqual(result, expected)

这做了几件重要的事情

  • 定义了 Consumer 和 Provider 对象,它们描述了我们的产品和我们要测试的服务
  • 使用 given 定义提供者的设置标准 UserA 存在且不是管理员
  • 定义了消费者期望发出的请求将包含的内容
  • 定义了服务器期望如何响应

使用 Pact 对象作为 上下文管理器,我们调用要测试的方法,然后它与 Pact 模拟通信。模拟将用我们定义的项目响应,使我们能够断言该方法处理了响应并返回了预期的值。

如果您想要对模拟的配置和交互验证有更多的控制,请分别使用 setupverify 方法

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 test
    result = get_user('UserA')
    # Some additional steps before verifying all interactions have occurred
finally:
    pact.verify()

关于 pact 关系定义的重要说明

您可能已经注意到,在我们的示例中 pact 关系是在模块级别定义的

pact = Consumer('Consumer').has_pact_with(Provider('Provider'))

这是因为它 只能在每个测试套件中做一次。默认情况下,当定义这种关系时,会清除 pact 文件,所以如果您在每个测试套件中定义它超过一次,您最终只会存储每个关系的 最后 声明的 pact。有关此主题的更多信息,请参阅 编写多个 pact

请求

在定义您代码期望发出的预期 HTTP 请求时,您可以指定方法、路径、正文、标题和查询

pact.with_request(
    method='GET',
    path='/api/v1/my-resources/',
    query={'search': 'example'}
)

query 用于指定 URL 查询参数,因此上述示例期望一个请求发送到 /api/v1/my-resources/?search=example

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()选项

has_pact_with(provider...)调用在其API中记录了许多选项,但有一些特别值得提及

version声明了提供者支持的pact规范版本。默认为"2.0.0",但如果您的提供者支持Pact规范版本3,则"3.0.0"也是可接受的。

from pactman import Consumer, Provider
pact = Consumer('Consumer').has_pact_with(Provider('Provider'), version='3.0.0')

file_write_mode默认为"overwrite",应该是"overwrite""merge"。Overwrite确保在调用has_pact_with()时删除任何现有的pact文件。Merge将保留pact文件并添加新的pact到该文件。参见编写多个pact。如果您绝对不想编写pact文件,请使用"never"

use_mocking_server默认为False,并控制pactman使用的mocking方法。默认情况下,它将修补urllib3,这是requests的底层库,也被一些其他项目使用。如果您正在使用不使用urllib3的库来制作HTTP请求,则需要将use_mocking_server参数设置为True。这将导致pactman运行实际的HTTP服务器来mock请求(服务器正在监听pact.uri - 使用它来将HTTP请求重定向到mock服务器。)您还可以将PACT_USE_MOCKING_SERVER环境变量设置为"yes",以强制整个套件使用服务器方法。您应在测试之外声明pact参与者(消费者和提供者),并且还需要在测试之外启动和停止mocking服务。下面的代码显示了使用服务器可能的样子

import atexit
from pactman import Consumer, Provider
pact = Consumer('Consumer').has_pact_with(Provider('Provider'), use_mocking_server=True)
pact.start_mocking()
atexit.register(pact.stop_mocking)

然后您可以使用pact来声明这些参与者之间的pact。

编写多个pact

在测试运行期间,您可能需要为消费者/提供者关系编写多个pact交互。以下是如何管理pact文件

  • 当调用has_pact_with()时,它将默认删除为指定的消费者和提供者创建的任何现有的pact JSON文件。
  • 您可以在测试开始时调用一次Consumer('Consumer').has_pact_with(Provider('Provider'))。这可以是pytest模块或会话固定装置,或者通过其他机制完成,并存储在一个变量中。按照惯例,我们在所有示例中都将其称为pact
  • 如果不适用,您可以手动指示has_pact_with()保留(file_write_mode="merge")或删除(file_write_mode="overwrite")现有的pact文件。

关于given()的一些话

您使用given()来告知提供者,他们应该具有某种状态,以便能够满足交互。您应在与提供者讨论后达成状态及其规范的共识。

如果您正在定义版本3的pact,您可以更丰富地定义提供者状态,例如

(pact
    .given("this is a simple state as in v2")
    .and_given("also the user must exist", username="alex")
)

现在您可以为提供者状态文本指定额外的参数。这些作为关键字参数传递,它们是可选的。您还可以使用and_given()调用提供额外的提供者状态,如果需要,可以多次调用。它与given()具有相同的调用约定:提供者状态名称和任何可选参数。

预期变量内容

默认的等值测试对始终静态的用户信息效果很好,但如果用户有一个设置为每次修改对象时都是当前时间的最后更新字段,会发生什么?为了处理可变数据和使您的测试更健壮,有几个有用的匹配器

Includes(matcher, sample_data)

版本3.0.0+ pacts可用

断言值应该包含给定的子字符串,例如:

from pactman import Includes, Like
Like({
    'id': 123, # match integer, value varies
    'content': Includes('spam', 'Sample spamming content')  # content must contain the string "spam"
})

消费者和提供者根据是否在 pact 的 with_request()will_respond_with() 部分使用 matchersample_data,它们的使用方式有所不同。使用上述示例

包含在请求中

当运行消费者的测试时,模拟将验证消费者在请求中使用的数据是否包含 matcher 字符串,如果无效则引发 AssertionError。当提供者验证合同时,将使用 sample_data 在请求实际提供者服务中,在这种情况下为 'Sample spamming content'

包含在响应中

当运行消费者的测试时,模拟将返回您提供的 sample_data,在这种情况下为 'Sample spamming content'。当在提供者上验证合同时,将从实际提供者服务返回的数据验证以确保它包含 matcher 字符串。

术语(matcher, sample_data)

断言值应匹配给定的正则表达式。您可以使用此功能在请求或响应中期望具有特定格式的时间戳,其中您知道需要特定格式,但不关心确切的日期。

from pactman import Term

(pact
 .given('UserA exists and is not an administrator')
 .upon_receiving('a request for UserA')
 .with_request(
   'post',
   '/users/UserA/info',
   body={'commencement_date': Term('\d+-\d+-\d', '1972-01-01')})
 .will_respond_with(200, body={
    'username': 'UserA',
    'last_modified': Term('\d+-\d+-\d+T\d+:\d+:\d+', '2016-12-15T20:16:01')
 }))

消费者和提供者根据是否在 pact 的 with_request()will_respond_with() 部分使用 matchersample_data,它们的使用方式有所不同。使用上述示例

请求中的术语

当运行消费者的测试时,模拟将验证消费者在请求中使用的 commencement_date 是否与 matcher 匹配,如果无效则引发 AssertionError。当提供者验证合同时,将使用 sample_data 在请求实际提供者服务中,在这种情况下为 1972-01-01

响应中的术语

当运行消费者的测试时,模拟将返回您提供的 last_modified 作为 sample_data,在这种情况下为 2016-12-15T20:16:01。当在提供者上验证合同时,将使用正则表达式搜索实际提供者服务的响应,如果正则表达式在响应中找到匹配项,则测试被视为成功。

Like(sample_data)

断言元素的类型与 sample_data 匹配。例如

from pactman import Like
Like(123)  # Matches if the value is an integer
Like('hello world')  # Matches if the value is a string
Like(3.14)  # Matches if the value is a float

请求中的 Like

当运行消费者的测试时,模拟将验证值是否为正确的类型,如果无效则引发 AssertionError。当提供者验证合同时,将使用 sample_data 在请求实际提供者服务中。

响应中的 Like

当运行消费者的测试时,模拟将返回 sample_data。当在提供者上验证合同时,将检查提供者服务生成的内容是否与 sample_data 的类型匹配。

将 Like 应用到复杂数据结构

当字典用作 Like 的参数时,所有子对象(以及它们的子对象等)将根据它们的类型进行匹配,除非您使用更具体的匹配器,如 Term。

from pactman import Like, Term
Like({
    'username': Term('[a-zA-Z]+', 'username'),
    'id': 123, # integer
    'confirmed': False, # boolean
    'address': { # dictionary
        'street': '200 Bourke St' # string
    }
})

EachLike(sample_data, minimum=1)

断言值是一个由类似 sample_data 的元素组成的数组类型。它可以用于断言简单的数组

from pactman import EachLike
EachLike(1)  # All items are integers
EachLike('hello')  # All items are strings

或嵌套其他匹配器以断言更复杂的对象

from pactman import EachLike, Term
EachLike({
    'username': Term('[a-zA-Z]+', 'username'),
    'id': 123,
    'groups': EachLike('administrators')
})

注意,您不需要在 JSON 响应中指定提供者将返回的所有内容,任何接收到的额外数据都将被忽略,并且测试仍然通过。

更多信息请参阅 匹配

使用 Equals 强制相等匹配

版本3.0.0+ pacts可用

如果您有一个需要与默认有效性测试匹配的精确值的 Like 子术语,则可以使用 Equals,例如:

from pactman import Equals, Like
Like({
    'id': 123, # match integer, value varies
    'username': Equals('alex')  # username must always be "alex"
})

Body 负载规则

body 负载假定是 JSON 数据。在没有 Content-Type 标头的情况下,我们假设 Content-Type: application/json; charset=UTF-8(JSON 文本是 Unicode,默认编码是 UTF-8)。

在验证过程中,非JSON有效载荷将被比较以检查它们是否相等。

在模拟过程中,HTTP响应将被处理为

  1. 如果没有 Content-Type 头部,则假定是JSON:使用 json.dumps() 序列化,编码为UTF-8,并添加头部 Content-Type: application/json; charset=UTF-8
  2. 如果存在 Content-Type 头部,并且它表明是 application/json,则使用 json.dumps() 序列化,并使用头部中的字符集,默认为UTF-8。
  3. 否则,将按原样传递 Content-Type 头部和主体。不支持二进制数据。

验证服务与 Pact 的对比

您有两个选项来验证与服务创建的契约

  1. 使用命令行程序 pactman-verifier,该程序将契约断言回放至您服务的运行实例,或者
  2. 使用集成到 pactman 中的 pytest 支持,将契约作为测试用例回放,允许使用其他测试机制,例如模拟和事务控制。

使用 pactman-verifier

运行 pactman-verifier -h 以查看可用选项。要运行注册到 Pact Broker 的提供者中的所有契约

pactman-verifier -b http://pact-broker.example/ <provider name> <provider url> <provider setup url>

您可以使用 -l 传递本地契约文件,这将验证服务与本地文件而不是代理

pactman-verifier -l /tmp/localpact.json <provider name> <provider url> <provider setup url>

您可以使用 --custom-provider-header 传递要传递给提供者状态设置和验证调用的头部。它可以多次使用

pactman-verifier -b <broker url> --custom-provider-header "someheader:value" --custom-provider-header
"this:that" <provider name> <provider url> <provider state url>

还可以在 PROVIDER_EXTRA_HEADER 环境变量中提供额外的头部,尽管命令行参数将覆盖此设置。

提供者状态

在许多情况下,您的契约将需要非常具体的数据存在于提供者上才能成功通过。如果您正在获取用户资料,则该用户需要存在;如果您正在查询记录列表,则至少需要存在一条记录。为了支持解耦消费者和提供者的测试,Pact 提出了提供者状态的概念,以便从消费者传达在提供者上应该存在哪些数据。

当设置提供者的测试时,您还需要设置这些提供者状态的管理。Pact 验证器通过向您提供的 <provider setup url> 发送额外的 HTTP 请求来实现这一点。此 URL 可以位于提供者应用程序中或单独的一个。一些管理状态的战略包括

  • 在您的应用程序中具有在生产环境中不活动的端点,用于创建和删除您的数据存储状态
  • 一个单独的应用程序,它有权访问相同的数据存储以创建和删除,例如指向同一数据存储的单独 App Engine 模块或 Docker 容器
  • 一个独立的应用程序,可以启动和停止其他服务器以及不同的数据存储状态

有关提供者状态的更多信息,请参阅 Pact 文档 中的 提供者状态

使用 pytest 验证契约

要验证提供者的契约,您需要在提供者测试套件中编写一个新的 pytest 测试模块。如果您不希望在常规单元测试运行中执行它,可以将其命名为 verify_pacts.py

您的测试代码需要使用 pactman 提供的 pact_verifier 修复程序,通过调用它的 verify() 方法并传递指向您服务运行实例的 URL(pytest-django 提供了一个方便的 live_server 修复程序,这里效果很好)以及一个用于设置提供者状态的回调(如下所述)。

您需要向 pytest 包括一些额外的命令行参数(如下所述),以指示契约的来源,以及是否将验证结果发布到契约代理。

一个 Django 项目的示例可能包含

from django.contrib.auth.models import User
from pactman.verifier.verify import ProviderStateMissing

def provider_state(name, **params):
    if name == 'the user "pat" exists':
        User.objects.create(username='pat', fullname=params['fullname'])
    else:
        raise ProviderStateMissing(name)

def test_pacts(live_server, pact_verifier):
    pact_verifier.verify(live_server.url, provider_state)

pact_verifier.verify 调用还可以接受第三个参数,以在验证期间向服务器发送额外的 HTTP 头部 - 将其指定为字典。

测试函数可以使用标准pytest fixtures进行任何级别的模拟和数据设置 - 因此,可以使用标准monkeypatching对下游API或其他提供者内的交互进行模拟。

使用pytest的提供者状态

传递给pact_verifier.verifyprovider_state函数将接收到所有待验证的pact的providerStateproviderStates

  • 对于具有providerState的pact,name参数将是providerState的值,而params将为空。
  • 对于具有providerStates的pact,该函数将对providerStates数组中的每个条目调用一次,name参数将从数组条目的name参数中获取,而params将从params参数中获取。

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

编写完pytest代码后,您需要使用附加参数调用pytest

--pact-broker-url=<URL>提供了用于从提供者处检索pact的Pact代理的基础URL。您还必须提供--pact-provider-name=<ProviderName>以标识从代理中检索pact的提供者。

代理URL和提供者名称也可以通过环境变量PACT_BROKER_URLPACT_PROVIDER_NAME提供。

您可以通过提供--pact-verify-consumer=<ConsumerName>来限制仅验证该消费者。与命令行验证器一样,您可以在代理URL中提供基本认证详情,或通过PACT_BROKER_AUTH环境变量提供。如果您的代理需要承载令牌,您可以使用--pact-broker-token=<TOKEN>PACT_BROKER_TOKEN环境变量提供。

--pact-files=<file pattern>验证一些通过通配符模式(Unix glob模式匹配)标识的磁盘上的pact JSON文件。

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

例如

# verify some local pacts in /tmp/pacts
$ pytest --pact-files=/tmp/pacts/*.json tests/verify_pacts.py

# verify some pacts in a broker for the provider MyService
$ pytest --pact-broker-url=http://pact-broker.example/ --pact-provider-name=MyService tests/verify_pacts.py

如果您需要查看导致pact失败的堆栈跟踪,可以使用pytest的详细程度标志(pytest -v)。

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

Pact代理配置

您还可以通过环境变量PACT_BROKER_URL指定代理URL。

如果代理需要HTTP基本认证,则可以在URL中提供该认证

pactman-verifier -b http://user:password@pact-broker.example/ ...
pytest --pact-broker-url=http://user:password@pact-broker.example/ ...

或在PACT_BROKER_AUTH环境变量中设置为user:password

如果您的代理需要承载令牌,则您可以在命令行中提供该令牌或在环境变量PACT_BROKER_TOKEN中设置它。

按标签过滤代理pact

如果您的消费者pact具有标签(称为“消费者版本标签”,因为它们附加到特定版本),则您可以在命令行中指定要检索pact的标签。可以指定多个标签,并且将验证所有匹配指定标签的pact。例如,为了确保您正在验证您的提供者与消费者的生产版本pact,请使用

pactman-verifier --consumer-version-tag=production -b http://pact-broker.example/ ...
pytest --pact-verify-consumer-tag=production --pact-broker-url=http://pact-broker.example/ ...

开发

请阅读CONTRIBUTING.md

发布历史

3.0.0 (未来,弃用警告)

  • 删除已弃用的--pact-consumer-name命令行选项

2.31.0

  • 修复了在代理元数据中Pact规范版本为4.0时无法解析为语义版本的问题

2.30.0

  • DELETE请求现在可以具有查询字符串,感谢@MazeDeveloper
  • 如果命令行上未指定pact源,则提供更友好的反馈,感谢@artamonovkirill
  • 将PACT_PROVIDER_NAME添加到环境变量中,感谢@artamonovkirill

2.29.0

  • 添加了对**递归通配符与--pact-files的支持,感谢@maksimt

2.28.0

  • 修复了在精确匹配中未调用fail()的边缘情况,导致pytest报告器不知道发生了失败
  • 解决了semver中对semver.parse的弃用问题
  • 停止了Python 3.6测试,添加了Python 3.8测试

2.27.0

  • 修复pytest插件中的错别字,防止--pact-verify-consumer-tag工作
  • 添加了PATCH支持

2.26.0

  • 允许pytest验证指定extra_provider_headers

2.25.0

  • 添加选项,即使pact验证失败,也允许pytest成功

2.24.0

  • 在pytest中更好地整合pact失败信息

2.23.0

  • 启用连接到pact代理时设置认证凭据
  • 允许通过消费者版本标签过滤从代理获取的pacts
  • 改进pytest命令行选项的命名和组织

2.22.0

  • 改进2.21.0版本中的更改

2.21.0

  • 在命令行输出处理器中处理警告级别消息

2.20.0

  • 修复pytest模式,以正确检测数组元素规则失败作为pytest失败
  • 允许使用--pact-consumer-name将pytest验证运行限制为单个消费者

2.19.0

  • 正确清理使用多个交互(使用with interaction1, interaction2而不是with pact)的pact上下文管理器

2.18.0

  • 修复清理过程中引入的bug,导致urllib模拟失效

2.17.0

  • 处理pytest设置中缺少任何提供者状态(!)

2.16.0

  • 延迟检查pacts目录的勾当,直到pacts实际写入,允许在模块级别定义pact而不产生副作用

2.15.0

  • 修复头匹配规则序列化结构
  • "never"添加到file_write_mode选项中
  • 处理x-www-form-urlencoded POST请求体

2.14.0

  • 改进详细消息,使其更清晰地说明其含义

2.13.0

  • 添加在验证期间向提供者提供额外头部的功能(感谢@ryallsa)

2.12.1

  • 修复pact-python Term兼容性

2.12.0

  • 为pact v3+添加EqualsIncludes匹配器
  • 如果交互中指定了缺少的头部,则验证失败
  • 显著改进了对pytest提供者验证pacts的支持
  • 将pact状态调用失败转换为警告而不是错误

2.11.0

  • 确保查询参数值是列表

2.10.0

  • 允许has_pact_with()接受file_write_mode
  • 修复2.9.0中引入的bug,其中生成多个pacts会导致记录单个pact

2.9.0

  • 修复当使用字典查询时调用with_request的bug(感谢Cong)
  • 在非服务器模拟中使start_mocking()stop_mocking()成为可选操作
  • 添加快捷方式,使python -m pactman.verifier.command_line变为python -m pactman(在发布前主要用于测试)
  • 处理None提供者状态
  • 确保所有用于生成pact文件的模拟中都使用一致的pact规范版本

2.8.0

  • 在模拟期间关闭一些边缘情况,并在README中记录

2.7.0

  • 添加and_given()作为定义v3+ pacts的额外提供者状态的方法
  • 添加更多pact生成(序列化)测试,修复了一些边缘案例bug
  • 修复verifier中处理小写HTTP方法的处理(感谢Cong!)

2.6.1

  • 修复模拟urlopen没有正确处理正确数量的位置参数的问题

2.6.0

  • 修复由于在多个测试用例中未检测到失败而引起的几个问题(头部、路径和数组元素规则可能没有应用)
  • 修复应用于数组中单个非第一个元素的规则
  • 修复<v3 pacts中消费者/提供者名称的生成

2.5.0

  • 修复围绕空数组验证的一些bug

2.4.0

  • 如果不存在且其父目录存在,则创建pact目标目录

2.3.0

  • 修复围绕模拟请求查询和模拟的验证的一些问题
  • 修复模拟验证中的头正则表达式匹配
  • 实际上使用传递给has_pact_with()的版本
  • 修复一些pact v3生成问题(感谢pan Jacek)

2.2.0

  • 恢复丢失的结果输出。

2.1.0

  • 当请求中没有body时,正确定义请求有效载荷

2.0.0

  • 在发布到代理时,正确确定pact验证结果。

1.2.0

  • 更正命令行错误处理中format_path的定义。
  • 调整README以提高清晰度。

1.1.0

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

1.0.8

  • 在项目元数据中更正了项目URL(感谢Jonathan Moss)
  • 修复了冗长输出

1.0.7

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

1.0.6

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

1.0.5

  • 更正了一些打包问题

1.0.4

  • pactman的初始版本,包括ReeceTech的pact-verifier版本3.17和pact-python版本0.17.0

项目详情


下载文件

下载适合您平台的应用程序。如果您不确定选择哪个,请了解更多关于安装包的信息。

源分发

pactman-2.31.0.tar.gz (77.3 kB 查看哈希)

上传时间

由以下支持