编写消费者测试
本教程将指导您从头开始编写消费者测试。首先,消费者测试使用 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 函数定义,该函数接受 PactOptions 和 PactFn。
import { pactWith } from 'jest-pact';
pactWith(PactOptions, PactFn);PactOptions 参数
使用 jest-pact 的 PactOptions 引入了 额外的选项,这些选项构建在 pact-js 提供的选项之上。在大多数情况下,您需要为这些测试定义 consumer、provider、log 和 dir 选项。
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.describe、Jest.beforeEach 和 Jest.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 定义四个属性:
state:描述发出请求前的先决条件状态。uponReceiving:描述此Interaction处理的请求类型。withRequest:定义请求规范的位置。它包含请求的method、path以及任何headers、body或query。willRespondWith:定义预期响应的位置。它包含响应的status、headers和body。
定义 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 的文件,您将在其中放置 request 和 response 定义。
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);
});
});
},
);