消費者テストの作成
このチュートリアルでは、ゼロからコンシューマテストを書く方法を説明します。MergeRequests#show
まず始めに、コンシューマーテストはpact-js
の上に構築されるjest-pact
を使用して記述します。 このチュートリアルでは、/discussions.json
REST API エンドポイント (/:namespace_name/:project_name/-/merge_requests/:id/discussions.json
) のコンシューマーテストを記述する方法を示します。GraphQL コンシューマー テストの例については、spec/contracts/consumer/specs/project/pipelines/show.spec.js
を参照してください。
スケルトンの作成
コンシューマテストのスケルトンを作成することから始めます。これはMergeRequests#show
ページのリクエストのためなので、spec/contracts/consumer/specs/project/merge_requests
の下にshow.spec.js
というファイルを作成します。そして、そのファイルに次の関数とパラメータを入力します:
コントラクトテストのディレクトリがどのように構成されているかについては、テストスイートフォルダの構成を参照してください。
pactWith
関数
Pact消費者テストは、PactOptions
とPactFn
を受け取るpactWith
関数を通して定義されます。
import { pactWith } from 'jest-pact';
pactWith(PactOptions, PactFn);
PactOptions
。
PactOptions
をjest-pact
で指定すると、pact-js
](https://github.com/pact-foundation/pact-js#constructor) で指定した[の上に追加オプションが追加されます。 ほとんどの場合、これらのテスト用に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.describe
,Jest.beforeEach
,Jest.it
のような Jest の標準メソッドを使用します。詳細は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
に4つの属性を定義します:
-
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);
});
});
},
);