【问题标题】:Mocking a system dependencies模拟系统依赖
【发布时间】:2022-02-23 16:49:04
【问题描述】:

我正在开发一个调用外部 API 的系统,其中一些由我的公司拥有,而另一些则不是。

我的系统由一个 HTTP 接口组成,该接口接受订单并将其发布到消息队列中以运行操作链。我的系统由 3 个 NodeJS 进程(1 个用于 HTTP,2 个消息队列消费者)、2 个数据库和一个消息队列组成。

当我开发我的应用程序时,很难测试我的系统涵盖的所有场景(即使我有单元测试)。为了确保所有组件协同工作,我正在使用 Gherkin 语言和 cucumber js 编写规范。

为了测试系统,我想和部署环境一样接近,所以我用 docker-compose 启动了我所有的系统,包括数据库、NodeJS 进程和消息队列。系统的所有组件都通过 docker-compose 配置中定义的 docker 网络进行通信。

问题是我无法确保所有外部 API 都处于准备好接受我的请求的正确状态,并且它们会以对我的测试步骤感兴趣的方式响应。

所以,我考虑为我的每个依赖项使用 Mock 服务器并发现了 pact.io。据我了解,Pact 允许我编写合约并启动一个模拟服务器,这样我的系统就可以对模拟服务器运行 HTTP 请求。 Pact 还允许我将合同提供给服务提供商,以便它还可以针对真实应用运行合同,看看它是否真的有效。

我看到了 javascript 中的示例,我能够启动模拟服务,为其提供交互,验证交互并关闭模拟服务。 (JS with mocha example)

我的问题是我希望我的系统与生产系统一样接近,因此我希望它通过我的 docker 网络访问 Pact 模拟服务。我看到了一个 Pact CLI docker 映像来运行 pact 模拟服务 (Pact CLI docker image),但是一旦我的模拟服务器被 docker 化,我就失去了使用 JS 包装器对add new interactions 的控制。

另外,我不想编写协议文件,我想在测试运行时添加交互,否则我将声明测试数据两次(一次在黄瓜测试场景中,一次在协议文件中)。

我的问题是:

有没有办法将 JS 包装器绑定到现有的模拟服务,一个 dockerize 服务? 使用 docker pact 镜像时,有没有办法在运行时添加交互? 因为我只需要一个模拟服务,所以 pact 是正确的工具吗?

编辑

我只是创建了一个沙盒环境来看看 NodeJS 包装器可以做什么。看来您可以使用 docker 创建一个模拟服务,并通过 NodeJS 包装器对其进行控制。

# Starts the docker container 

docker run -dit \
  --rm \
  --name pact-mock-service \
  -p 1234:1234 \
  -v <YOUR_PATH>/docker_pacts/:/tmp/pacts \
  pactfoundation/pact-cli:latest \
  mock-service \
  -p 1234 \
  --host 0.0.0.0 \
  --pact-dir /tmp/pacts
const {Pact, MockService, } = require('@pact-foundation/pact') 
const axios = require('axios')

const pact = new Pact({
  consumer: "my client",
  provider: "some provider",
  // Those two are ignored since we override the inner mock service
  port: 1234,
  host: 'localhost'
})


const mockService = new MockService(
  // You need to duplicate those data, normally they are passed
  // by the pact object when calling `pact.setup()`.
  'my client', 
  'provider', 
  // the port and host to the docker container
  1234, 
  'localhost'
)

pact.mockService = mockService

async function run () {
  await pact.addInteraction({
    state: "some data is created",
    withRequest: {
      method: "GET",
      path: "/hello"
    },
    willRespondWith: {
      status: 200,
      body: {
        hello: 'hello world'
      }
    },
    uponReceiving: ''
  })

  const response = await axios.get('http://localhost:1234/hello')

  console.log(response.data) // { "hello": "hello world" }
}

run().catch(console.error)

编辑 2

我可能会按照 Matthew Fellows 的回答,使用 Pact 模拟我系统的外部交互的某种单元测试来测试我的系统。

【问题讨论】:

    标签: cucumberjs pact


    【解决方案1】:

    所以,我考虑为我的每个依赖项使用 Mock 服务器并发现了 pact.io。据我了解,Pact 允许我编写合约并启动一个模拟服务器,这样我的系统就可以对模拟服务器运行 HTTP 请求。 Pact 还允许我将合同提供给服务提供商,以便它还可以针对真实应用运行合同,看看它是否真的有效。

    是的,没错。 Pact 可以被视为 API 客户端的单元测试工具,它使用契约测试来确保模拟有效。

    如果您仅将 Pact 用于模拟,那么您将错过所有关键优势。

    在如此高水平的测试中使用 Pact 被认为是不好的做法,而且正如您所见,这很难做到(您正在与预期的使用方式背道而驰)。

    我不太担心重叠测试(端到端测试总是会与设计的其他层测试重叠),并且更关心确保 Pact 涵盖每个 API 合同。这些测试将运行得更快,测试内容更精确,并且不那么不稳定。

    这样,您可以将端到端 BDD 场景的范围缩小到关键场景,从而降低维护成本。

    【讨论】:

    • 感谢您的回答。据我了解您的建议,我应该避免在我的高级黄瓜场景中涵盖需要模拟的场景。就我而言,需要涵盖的所有场景都涉及在我无法控制的服务中搜索外部数据。没有一个案例没有外部 API。
    • 不,我主张在 BDD 期间进行更多模拟,只是不要在这些测试中使用 Pact。 Pact 旨在更精确地使用(单元测试)。我会在 BDD 测试中找到所有你发现自己在模拟 API 客户端的地方,并为这些地方提供单独的合同测试(使用 Pact)
    • 好的,感谢您的精确!