Help us learn about your current experience with the documentation. Take the survey.

编写消费者测试

本教程将指导您从头开始编写消费者测试。首先,消费者测试使用 jest-pact 编写,它构建在 pact-js 之上。本教程将展示如何为 /discussions.json REST API 端点编写消费者测试,该端点位于 /:namespace_name/:project_name/-/merge_requests/:id/discussions.json,在 MergeRequests#show 页面中被调用。有关 GraphQL 消费者测试的示例,请参见 spec/contracts/consumer/specs/project/pipelines/show.spec.js

创建骨架

首先创建消费者测试的骨架。由于这是为 MergeRequests#show 页面的请求编写的,请在 spec/contracts/consumer/specs/project/merge_requests 下创建一个名为 show.spec.js 的文件。 然后,用以下函数和参数填充它:

有关测试套件文件夹结构的更多信息,请参见 测试套件文件夹结构

pactWith 函数

Pact 消费者测试通过 pactWith 函数定义,该函数接受 PactOptionsPactFn

import { pactWith } from 'jest-pact';

pactWith(PactOptions, PactFn);

PactOptions 参数

使用 jest-pactPactOptions 引入了 额外的选项,这些选项构建在 pact-js 提供的选项之上。在大多数情况下,您需要为这些测试定义 consumerproviderlogdir 选项。

import { pactWith } from 'jest-pact';

pactWith(
  {
    consumer: 'MergeRequests#show',
    provider: 'GET discussions',
    log: '../logs/consumer.log',
    dir: '../contracts/project/merge_requests/show',
  },
  PactFn
);

有关如何命名消费者和提供者的更多信息,请参见 命名约定

PactFn 参数

PactFn 是您定义测试的地方。在这里您可以设置模拟提供者,并可以使用标准的 Jest 方法,如 Jest.describeJest.beforeEachJest.it。更多信息请参见 https://jestjs.io/docs/api

import { pactWith } from 'jest-pact';

pactWith(
  {
    consumer: 'MergeRequests#show',
    provider: 'GET discussions',
    log: '../logs/consumer.log',
    dir: '../contracts/project/merge_requests/show',
  },

  (provider) => {
    describe('GET discussions', () => {
      beforeEach(() => {

      });

      it('return a successful body', async () => {

      });
    });
  },
);

设置模拟提供者

在运行测试之前,设置模拟提供者来处理指定的请求并返回指定的响应。为此,在 Interaction 中定义状态、预期请求和响应。

在本教程中,为 Interaction 定义四个属性:

  1. state:描述发出请求前的先决条件状态。
  2. uponReceiving:描述此 Interaction 处理的请求类型。
  3. withRequest:定义请求规范的位置。它包含请求的 methodpath 以及任何 headersbodyquery
  4. willRespondWith:定义预期响应的位置。它包含响应的 statusheadersbody

定义 Interaction 后,通过调用 addInteraction 将该交互添加到模拟提供者。

import { pactWith } from 'jest-pact';
import { Matchers } from '@pact-foundation/pact';

pactWith(
  {
    consumer: 'MergeRequests#show',
    provider: 'GET discussions',
    log: '../logs/consumer.log',
    dir: '../contracts/project/merge_requests/show',
  },

  (provider) => {
    describe('GET discussions', () => {
      beforeEach(() => {
        const interaction = {
          state: 'a merge request with discussions exists',
          uponReceiving: 'a request for discussions',
          withRequest: {
            method: 'GET',
            path: '/gitlab-org/gitlab-qa/-/merge_requests/1/discussions.json',
            headers: {
              Accept: '*/*',
            },
          },
          willRespondWith: {
            status: 200,
            headers: {
              'Content-Type': 'application/json; charset=utf-8',
            },
            body: Matchers.eachLike({
              id: Matchers.string('fd73763cbcbf7b29eb8765d969a38f7d735e222a'),
              project_id: Matchers.integer(6954442),
              ...
              resolved: Matchers.boolean(true)
            }),
          },
        };
        provider.addInteraction(interaction);
      });

      it('return a successful body', async () => {

      });
    });
  },
);

响应体 Matchers

请注意我们在预期响应的 body 中如何使用 Matchers。这使我们能够足够灵活地接受不同的值,但又足够严格地区分有效值和无效值。我们必须确保我们有严格的定义,既不太严格也不太宽松。阅读有关 不同类型的 Matchers 的更多信息。我们目前正在使用 V2 匹配规则。

编写测试

设置好模拟提供者后,就可以编写测试了。对于此测试,您发出请求并期望特定的响应。

首先,设置进行 API 请求的客户端。为此,创建 spec/contracts/consumer/resources/api/project/merge_requests.js 并添加以下 API 请求。如果端点是 GraphQL,则我们在 spec/contracts/consumer/resources/graphql 下创建它。

import axios from 'axios';

export async function getDiscussions(endpoint) {
  const { url } = endpoint;

  return axios({
    method: 'GET',
    baseURL: url,
    url: '/gitlab-org/gitlab-qa/-/merge_requests/1/discussions.json',
    headers: { Accept: '*/*' },
  })
}

设置完成后,将其导入测试文件并调用它来发出请求。然后,您可以发出请求并定义您的期望。

import { pactWith } from 'jest-pact';
import { Matchers } from '@pact-foundation/pact';

import { getDiscussions } from '../../../resources/api/project/merge_requests';

pactWith(
  {
    consumer: 'MergeRequests#show',
    provider: 'GET discussions',
    log: '../logs/consumer.log',
    dir: '../contracts/project/merge_requests/show',
  },

  (provider) => {
    describe('GET discussions', () => {
      beforeEach(() => {
        const interaction = {
          state: 'a merge request with discussions exists',
          uponReceiving: 'a request for discussions',
          withRequest: {
            method: 'GET',
            path: '/gitlab-org/gitlab-qa/-/merge_requests/1/discussions.json',
            headers: {
              Accept: '*/*',
            },
          },
          willRespondWith: {
            status: 200,
            headers: {
              'Content-Type': 'application/json; charset=utf-8',
            },
            body: Matchers.eachLike({
              id: Matchers.string('fd73763cbcbf7b29eb8765d969a38f7d735e222a'),
              project_id: Matchers.integer(6954442),
              ...
              resolved: Matchers.boolean(true)
            }),
          },
        };
      });

      it('return a successful body', async () => {
        const discussions = await getDiscussions({
          url: provider.mockService.baseUrl,
        });

        expect(discussions).toEqual(Matchers.eachLike({
          id: 'fd73763cbcbf7b29eb8765d969a38f7d735e222a',
          project_id: 6954442,
          ...
          resolved: true
        }));
      });
    });
  },
);

就这样!消费者测试现已设置完毕。现在您可以尝试 运行此测试

提高测试可读性

您可能已经注意到,请求和响应定义可能会变得很大。这导致测试难以阅读,需要大量滚动才能找到您想要的内容。您可以通过将这些提取到 fixture 中来使测试更易于阅读。

spec/contracts/consumer/fixtures/project/merge_requests 下创建一个名为 discussions.fixture.js 的文件,您将在其中放置 requestresponse 定义。

import { Matchers } from '@pact-foundation/pact';

const body = Matchers.eachLike({
  id: Matchers.string('fd73763cbcbf7b29eb8765d969a38f7d735e222a'),
  project_id: Matchers.integer(6954442),
  ...
  resolved: Matchers.boolean(true)
});

const Discussions = {
  body: Matchers.extractPayload(body),

  success: {
    status: 200,
    headers: {
      'Content-Type': 'application/json; charset=utf-8',
    },
    body,
  },

  scenario: {
    state: 'a merge request with discussions exists',
    uponReceiving: 'a request for discussions',
  },

  request: {
    withRequest: {
      method: 'GET',
      path: '/gitlab-org/gitlab-qa/-/merge_requests/1/discussions.json',
      headers: {
        Accept: '*/*',
      },
    },
  },
};

exports.Discussions = Discussions;

将所有这些移动到 fixture 后,您可以将测试简化为以下内容:

import { pactWith } from 'jest-pact';

import { Discussions } from '../../../fixtures/project/merge_requests/discussions.fixture';
import { getDiscussions } from '../../../resources/api/project/merge_requests';

const CONSUMER_NAME = 'MergeRequests#show';
const PROVIDER_NAME = 'GET discussions';
const CONSUMER_LOG = '../logs/consumer.log';
const CONTRACT_DIR = '../contracts/project/merge_requests/show';

pactWith(
  {
    consumer: CONSUMER_NAME,
    provider: PROVIDER_NAME,
    log: CONSUMER_LOG,
    dir: CONTRACT_DIR,
  },

  (provider) => {
    describe(PROVIDER_NAME, () => {
      beforeEach(() => {
        const interaction = {
          ...Discussions.scenario,
          ...Discussions.request,
          willRespondWith: Discussions.success,
        };
        provider.addInteraction(interaction);
      });

      it('return a successful body', async () => {
        const discussions = await getDiscussions({
          url: provider.mockService.baseUrl,
        });

        expect(discussions).toEqual(Discussions.body);
      });
    });
  },
);