消費者テストの作成

このチュートリアルでは、ゼロからコンシューマテストを書く方法を説明します。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消費者テストは、PactOptionsPactFn を受け取るpactWith 関数を通して定義されます。

import { pactWith } from 'jest-pact';

pactWith(PactOptions, PactFn);

PactOptions

PactOptionsjest-pact で指定すると、pact-js](https://github.com/pact-foundation/pact-js#constructor) で指定した[の上に追加オプションが追加されます。 ほとんどの場合、これらのテスト用に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.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つの属性を定義します:

  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

期待されるレスポンスのbodyMatchers を使っていることに注目してください。これによって、異なる値を受け入れるのに十分な柔軟性を保ちつつ、有効な値と無効な値を区別するのに十分な厳密性を保つことができます。厳しすぎず、甘すぎず、きっちりとした定義をしなければなりません。さまざまなタイプの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);
      });
    });
  },
);