InfoGrab DocsInfoGrab Docs

컨슈머 테스트 작성

요약

이 튜토리얼은 컨슈머 테스트를 처음부터 작성하는 과정을 안내합니다. 컨슈머 테스트의 스켈레톤을 먼저 작성합니다. 계약 테스트 디렉터리 구조에 대한 자세한 내용은 테스트 스위트 폴더 구조를 참고하세요. Pact 컨슈머 테스트는 PactOptions와 PactFn을 받는 pactWith 함수를 통해 정의합니다.

이 튜토리얼은 컨슈머 테스트를 처음부터 작성하는 과정을 안내합니다. 컨슈머 테스트는 pact-js를 기반으로 하는 jest-pact를 사용하여 작성합니다. 이 튜토리얼에서는 MergeRequests#show 페이지에서 호출되는 /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 파라미터#

jest-pact와 함께 사용하는 PactOptionspact-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은 테스트를 정의하는 곳입니다. 이 함수 내에서 목(mock) 프로바이더를 설정하고, 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에서 상태(state)와 예상 요청 및 응답을 정의합니다.

이 튜토리얼에서는 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 () => {

      });
    });
  },
);

응답 본문 Matcher#

예상 응답의 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하고 호출하여 요청을 만듭니다. 그런 다음 요청을 보내고 기대값을 정의할 수 있습니다.

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);
      });
    });
  },
);

컨슈머 테스트 작성

GitLab v19.1
원문 보기
요약

이 튜토리얼은 컨슈머 테스트를 처음부터 작성하는 과정을 안내합니다. 컨슈머 테스트의 스켈레톤을 먼저 작성합니다. 계약 테스트 디렉터리 구조에 대한 자세한 내용은 테스트 스위트 폴더 구조를 참고하세요. Pact 컨슈머 테스트는 PactOptions와 PactFn을 받는 pactWith 함수를 통해 정의합니다.

이 튜토리얼은 컨슈머 테스트를 처음부터 작성하는 과정을 안내합니다. 컨슈머 테스트는 pact-js를 기반으로 하는 jest-pact를 사용하여 작성합니다. 이 튜토리얼에서는 MergeRequests#show 페이지에서 호출되는 /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 파라미터#

jest-pact와 함께 사용하는 PactOptionspact-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은 테스트를 정의하는 곳입니다. 이 함수 내에서 목(mock) 프로바이더를 설정하고, 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에서 상태(state)와 예상 요청 및 응답을 정의합니다.

이 튜토리얼에서는 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 () => {

      });
    });
  },
);

응답 본문 Matcher#

예상 응답의 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하고 호출하여 요청을 만듭니다. 그런 다음 요청을 보내고 기대값을 정의할 수 있습니다.

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);
      });
    });
  },
);