프런트엔드 테스트 표준 및 스타일 가이드

프런트엔드 코드를 개발하면서 GitLab에서는 두 가지 유형의 테스트 스위트를 만날 수 있습니다. JavaScript 단위 및 통합 테스트에는 Jest를 사용하며 e2e (end-to-end) 통합 테스트에 대해 RSpec 기능 테스트와 Capybara를 사용합니다.

새로운 기능에는 단위 및 기능 테스트를 작성해야 합니다. 대부분의 경우 기능 테스트에는 RSpec를 사용해야 합니다. 기능 테스트 시작 방법에 대한 자세한 정보는 기능 테스트 시작를 참조하세요.

버그 수정을 위해 회귀 테스트를 작성하여 향후 재발을 방지해야 합니다.

일반적인 테스팅 관행에 대한 자세한 내용은 테스트 표준 및 스타일 가이드 페이지를 참조하세요.

Vue.js 테스트

Vue 컴포넌트 테스트 가이드를 찾고 계시다면 이 섹션으로 바로 이동할 수 있습니다.

Jest

Jest를 사용하여 프런트엔드 단위 및 통합 테스트를 작성합니다. Jest 테스트는 EE의 /spec/frontend/ee/spec/frontend에서 찾을 수 있습니다.

jsdom의 제한 사항

Jest는 테스트를 실행하기 위해 브라우저 대신 jsdom을 사용합니다. 이에는 몇 가지 제한 사항이 있습니다. 구체적으로 다음과 같습니다:

또한 브라우저에서 Jest 테스트를 실행하는 데 대한 지원 현황에 대한 문제도 참조하세요 (https://gitlab.com/gitlab-org/gitlab/-/issues/26982).

Jest 테스트 디버깅

yarn jest-debug를 실행하면 Jest를 디버그 모드로 실행하여 Jest 문서에 설명된 대로 디버깅/검사할 수 있습니다.

시간 초과 오류

Jest의 기본 제한 시간은 /jest.config.base.js에서 설정됩니다.

테스트가 해당 시간을 초과하면 실패합니다.

테스트의 성능을 개선할 수 없는 경우 테스트 스위트 전체의 제한 시간을 jest.setTimeout을 사용하여 증가시킬 수 있습니다.

jest.setTimeout(500);

describe('Component', () => {
  it('does something amazing', () => {
    // ...
  });
});

또는 세 번째 인수를 제공하여 특정 테스트의 시간 초과 시간을 증가시킴으로써 해당 특정 테스트에 대해 설정할 수 있습니다.

describe('Component', () => {
  it('does something amazing', () => {
    // ...
  }, 500)
})

각 테스트의 성능은 환경에 따라 다릅니다.

테스트별 스타일시트

RSpec 통합 테스트를 용이하게 하기 위해 두 가지 테스트별 스타일시트를 제공합니다. 이를 사용하여 테스트 속도를 향상시키거나 Capybara 클릭 이벤트의 대상이 되어야 하는 요소를 표시할 수 있습니다.

  • app/assets/stylesheets/disable_animations.scss
  • app/assets/stylesheets/test_environment.scss

테스트 환경은 가능한 한 프로덕션 환경과 일치해야 하므로 이러한 스타일시트를 최소한으로 사용하고 필요할 때만 추가해야 합니다.

무엇을 테스트하고 어떻게 테스트할 것인가

모의 및 감시와 같은 Jest별 워크플로에 대한 더 자세한 내용으로 들어가기 전에, Jest로 무엇을 테스트할 것인지 간략하게 다루어 보겠습니다.

라이브러리 테스트하지 마세요

라이브러리는 JavaScript 개발자에게 필수적인 부분입니다. 일반적인 조언은 라이브러리 내부를 테스트하지 말고, 라이브러리가 자신이 무엇을 해야 하며 자체적으로 테스트 커버리지를 갖고 있는 것을 기대해야 합니다. 일반적인 예시는 다음과 같습니다.

import { convertToFahrenheit } from 'temperatureLibrary'

function getFahrenheit(celsius) {
  return convertToFahrenheit(celsius)
}

getFahrenheit 함수를 테스트하는 것은 의미가 없습니다. 내부적으로 라이브러리 함수를 호출하는 것 외에는 아무 작업도 하지 않기 때문에 라이브러리 함수가 제대로 작동한다고 기대할 수 있습니다.

Vue 영역으로 짧게 살펴보겠습니다. Vue는 GitLab JavaScript 코드베이스의 중요한 요소입니다. Vue 컴포넌트에 대한 스펙을 작성할 때 흔히 발생하는 함정은 실제로 Vue가 제공하는 기능을 테스트하는 것으로 감이 잘 잡히기 때문에 그 부분을 테스트하는 것입니다. 아래는 코드베이스에서 가져온 예제입니다.

// 컴포넌트 스크립트
{
  computed: {
    hasMetricTypes() {
      return this.metricTypes.length;
    },
}
<!-- 컴포넌트 템플릿 -->
<template>
  <gl-dropdown v-if="hasMetricTypes">
    <!-- 드롭다운 컨텐츠 -->
  </gl-dropdown>
</template>

hasMetricTypes 계산된 속성을 테스트하는 것은 주어진 경우 Vue 라이브러리 자체를 테스트하는 것입니다. 이는 테스트 스위트에 추가되는 것을 제외하고는 가치가 없습니다. 사용자가 상호 작용하는 방식으로 컴포넌트를 테스트하는 것이 더 나은 방법입니다: 렌더링된 템플릿을 확인하는 것입니다.

잘못된 예시:

describe('computed', () => {
  describe('hasMetricTypes', () => {
    it('returns true if metricTypes exist', () => {
      factory({ metricTypes });
      expect(wrapper.vm.hasMetricTypes).toBe(2);
    });

    it('returns true if no metricTypes exist', () => {
      factory();
      expect(wrapper.vm.hasMetricTypes).toBe(0);
    });
  });
});

정답:

```javascript
it('메트릭 유형이 존재하면 드롭다운을 표시합니다', () => {
  factory({ metricTypes });
  expect(wrapper.findComponent(GlDropdown).exists()).toBe(true);
});

it('메트릭 유형이 없으면 드롭다운을 표시하지 않습니다', () => {
  factory();
  expect(wrapper.findComponent(GlDropdown).exists()).toBe(false);
});

이러한 종류의 테스트에 주의하세요. 이러한 테스트는 단순히 로직 업데이트를 더 취약하고 성가신 작업으로 만듭니다. 다른 라이브러리에도 이는 동일합니다. 여기서의 제안은: 만약 `wrapper.vm` 속성을 확인하고 있다면, 렌더링된 템플릿을 확인하기 위해 테스트를 재고해야 할 것입니다.

더 많은 예시는 [프런트엔드 단위 테스트 섹션](testing_levels.md#frontend-unit-tests)에서 확인할 수 있습니다.

### 모의 테스트(mock)를 테스트하지 마세요

또 다른 흔한 실수는 명세(specs)가 모의(mock)가 작동하는지 확인하게 되는 것입니다. 모의를 사용 중이라면, 모의는 테스트를 지원해야 하지만 테스트의 대상이 되어서는 안 됩니다.

```javascript
const spy = jest.spyOn(idGenerator, 'create')
spy.mockImplementation = () = '1234'

// 나쁨
expect(idGenerator.create()).toBe('1234')

// 좋음: 실제로 컴포넌트의 로직에 집중하고 통제 가능한 모의(mock)의 출력을 활용
expect(wrapper.find('div').html()).toBe('<div id="1234">...</div>')

사용자의 흔적 따라가기

컴포넌트가 많은 세계에서 단위 테스트와 통합 테스트 사이의 경계는 매우 희미할 수 있습니다. 가장 중요한 지침은 다음과 같습니다:

  • 미래에 깨질 수 있는 복잡한 로직을 테스트하는 것에 실제 가치가 있는 경우에는 고립시켜 깨짐을 방지하기 위해 깨끗한 단위 테스트를 작성합니다.
  • 그렇지 않으면 사용자의 흐름에 가능한 가깝게 귀결되도록 사양을 작성하려고 노력하세요.

예를 들어, 생성된 마크업을 사용하여 버튼 클릭을 유도하고 변경된 마크업을 확인하는 것이 수동으로 메서드를 호출하고 데이터 구조나 계산된 속성을 확인하는 것보다 나은 접근입니다. 테스트는 통과하고 가짜 안전감을 제공하지만 실수로 사용자 흐름을 망가뜨릴 가능성이 항상 있습니다.

일반적인 관행

우리의 테스트 스위트의 일부로 포함된 몇 가지 공통된 관행입니다. 이 안내서에 따르지 않은 사항이 발견된다면 가능한한 빨리 수정하는 것이 이상적입니다. 🎉

DOM 요소 쿼리 방법

테스트에서 DOM 요소를 쿼리할 때, 요소를 고유하고 의미적으로 타겟팅하는 것이 가장 좋습니다.

일반적으로, DOM Testing Library를 사용하여 사용자가 실제로 보는 것을 타겟팅하는 것이 좋습니다. 텍스트로 선택하는 경우 byRole 쿼리를 사용하는 것이 가장 좋습니다. findByRole 및 다른 DOM Testing Library 쿼리shallowMountExtended 또는 mountExtended를 사용할 때 사용할 수 있습니다.

Vue 컴포넌트 단위 테스트를 작성할 때, 단위 테스트가 아닌 디테일한 자식 컴포넌트 동작에 대처하는 대신에 자식 컴포넌트를 컴포넌트별로 쿼리하는 것이 현명할 수 있습니다.

가끔, 위의 방법들이 현실적이지 않을 수 있습니다. 이러한 경우에는 셀렉터를 단순화하기 위해 테스트 속성을 추가하는 것이 최선일 수 있습니다. 가능한 셀렉터의 목록에는 다음이 있습니다:

import { shallowMountExtended } from 'helpers/vue_test_utils_helper'

const wrapper = shallowMountExtended(ExampleComponent);

it('exists', () => {
  // 최상 (특히 통합 테스트의 경우)
  wrapper.findByRole('link', { name: /Click Me/i })
  wrapper.findByRole('link', { name: 'Click Me' })
  wrapper.findByText('Click Me')
  wrapper.findByText(/Click Me/i)

  // 좋음 (특히 단위 테스트의 경우)
  wrapper.findComponent(FooComponent);
  wrapper.find('input[name=foo]');
  wrapper.find('[data-testid="my-foo-id"]');
  wrapper.findByTestId('my-foo-id'); // with shallowMountExtended or mountExtended – check below
  wrapper.find({ ref: 'foo'});

  // 나쁨
  wrapper.find('.js-foo');
  wrapper.find('.btn-primary');
});

data-testid 속성에는 kebab-case를 사용해야 합니다.

테스트를 위한 목적으로 .js-* 클래스를 추가하는 것은 권장되지 않습니다. 다른 가능한 옵션이 없는 경우에만 사용하세요.

자식 컴포넌트 쿼리

@vue/test-utils를 사용하여 Vue 컴포넌트를 테스트할 때, 다른 접근 방식은 DOM 노드를 쿼리하는 대신에 자식 컴포넌트를 쿼리하는 것입니다. 이는 테스트하는 동작의 구현 세부사항이 해당 컴포넌트의 개별 단위 테스트에 의해 다뤄지는 것을 가정합니다. 컴포넌트 또는 DOM 쿼리를 작성하는 것에는 뚜렷한 선호사항이 없으며 테스트가 신뢰성 있게 대상 컴포넌트의 예상 동작을 다루는 한 모두 적절합니다.

예시:

it('exists', () => {
  wrapper.findComponent(FooComponent);
});

단위 테스트 명명

특정 기능/메서드를 테스트하는 설명 블록을 작성할 때는 메서드 이름을 설명 블록 이름으로 사용하세요.

나쁨:

describe('#methodName', () => {
  it('passes', () => {
    expect(true).toEqual(true);
  });
});

describe('.methodName', () => {
  it('passes', () => {
    expect(true).toEqual(true);
  });
});

좋음:

describe('methodName', () => {
  it('passes', () => {
    expect(true).toEqual(true);
  });
});

Promises 테스트

Promise를 테스트할 때는 항상 테스트가 비동기적이고 거부(rejection)가 처리되었는지 확인해야합니다. 이제 테스트 스위트에서 async/await 구문을 사용할 수 있습니다.

it('Promise를 테스트합니다', async () => {
  const users = await fetchUsers()
  expect(users.length).toBe(42)
});

it('Promise 거부를 테스트합니다', async () => {
  await expect(user.getUserName(1)).rejects.toThrow('User with 1 not found.');
});

또한 테스트 함수에서도 Promise를 반환할 수 있습니다.

donedone.fail 콜백을 사용하는 것은 promise와 함께 작업할 때 권장되지 않습니다. 사용해서는 안 됩니다.

나쁜 예시:

// return이 누락됨
it('Promise를 테스트합니다', () => {
  promise.then(data => {
    expect(data).toBe(asExpected);
  });
});

// done/done.fail을 사용함
it('Promise를 테스트합니다', done => {
  promise
    .then(data => {
      expect(data).toBe(asExpected);
    })
    .then(done)
    .catch(done.fail);
});

좋은 예시:

// 해결된 promise를 확인
it('Promise를 테스트합니다', () => {
  return promise
    .then(data => {
      expect(data).toBe(asExpected);
    });
});

// Jest의 `resolves` Matcher를 사용하여 해결된 promise를 확인
it('Promise를 테스트합니다', () => {
  return expect(promise).resolves.toBe(asExpected);
});

// Jest의 `rejects` Matcher를 사용하여 거부된 promise를 확인
it('Promise 거부를 테스트합니다', () => {
  return expect(promise).rejects.toThrow(expectedError);
});

시간 조작

때로는 시간에 민감한 코드를 테스트해야 할 때도 있습니다. 예를 들어 X 초마다 실행되는 반복 이벤트 등이 그 예입니다. 이에 대한 처리 전략은 다음과 같습니다:

애플리케이션에서의 setTimeout()/setInterval() 모의

애플리케이션이 시간을 기다리고 있다면, 대기를 모의화합니다. Jest에서는 이미 기본적으로 수행되었습니다 (또한 Jest Timer Mocks를 참조).

const doSomethingLater = () => {
  setTimeout(() => {
    // 무언가를 수행
  }, 4000);
};

Jest에서:

it('무언가를 수행합니다', () => {
  doSomethingLater();
  jest.runAllTimers();

  expect(something).toBe('done');
});

Jest에서 현재 위치 모의화

참고: window.location.href의 값은 이전 테스트가 이후 테스트에 영향을 미치지 않도록 각 테스트 전에 재설정됩니다.

테스트에 따라 특정한 값으로 window.location.href가 필요한 경우 setWindowLocation 도우미를 사용합니다.

import setWindowLocation from 'helpers/set_window_location_helper';

it('통과합니다', () => {
  setWindowLocation('https://gitlab.test/foo?bar=true');

  expect(window.location).toMatchObject({
    hostname: 'gitlab.test',
    pathname: '/foo',
    search: '?bar=true',
  });
});

해시만 수정하려면 setWindowLocation 도우미를 사용하거나 직접 window.location.hash에 할당합니다. 예:

it('통과합니다', () => {
  window.location.hash = '#foo';

  expect(window.location.href).toBe('http://test.host/#foo');
});

테스트에서 특정 window.location 메서드가 호출되었는지 확인해야 하는 경우 useMockLocationHelper 도우미를 사용합니다.

import { useMockLocationHelper } from 'helpers/mock_window_location_helper';

useMockLocationHelper();

it('통과합니다', () => {
  window.location.reload();

  expect(window.location.reload).toHaveBeenCalled();
});

테스트 대기

어떤 일이 발생하기를 기다려야 하는 경우도 있습니다.

다음을 피해야 합니다:

  • setTimeout은 대기 이유를 애매하게 만들기 때문에 피해야 합니다. 또한 테스트에서 가짜 처리되므로 사용법이 까다로워집니다.
  • setImmediate는 Jest 27 이후에서는 더 이상 지원되지 않습니다. 자세한 내용은 이 이슈를 참조하십시오.

Promises 및 Ajax 호출

Promise가 해결될 때까지 핸들러 함수를 등록합니다.

const askTheServer = () => {
  return axios
    .get('/endpoint')
    .then(response => {
      // 무언가를 수행
    })
    .catch(error => {
      // 다른 무언가를 수행
    });
};

Jest에서:

it('Ajax 호출을 기다립니다', async () => {
  await askTheServer()
  expect(something).toBe('done');
});

Promise에 핸들러를 등록할 수 없는 경우, 예를 들어 동기적인 Vue 라이프사이클 훅에서 실행되기 때문이라면, waitFor 도우미를 살펴보거나 모든 보류 중인 Promise을 플러시합니다.

Jest에서:

it('Ajax 호출을 기다립니다', async () => {
  synchronousFunction();

  await waitForPromises();

  expect(something).toBe('done');
});

Vue 렌더링

nextTick()을 사용하여 Vue 컴포넌트가 다시 렌더링될 때까지 기다릴 수 있습니다.

Jest에서:

import { nextTick } from 'vue';

// ...

it('렌더링되는지 확인', async () => {
  wrapper.setProps({ value: 'new value' });

  await nextTick();

  expect(wrapper.text()).toBe('new value');
});

이벤트

애플리케이션이 테스트 중에 기다려야 하는 이벤트를 트리거하는 경우, 단언을 포함하는 이벤트 핸들러를 등록하십시오:

it('이벤트를 기다립니다', () => {
  eventHub.$once('someEvent', eventHandler);

  someFunction();

  return new Promise((resolve) => {
    function expectEventHandler() {
      expect(something).toBe('done');
      resolve();
    }
  });
});

Jest에서 이와 같은 경우 Promise를 사용할 수도 있습니다:

it('이벤트를 기다립니다', () => {
  const eventTriggered = new Promise(resolve => eventHub.$once('someEvent', resolve));

  someFunction();

  return eventTriggered.then(() => {
    expect(something).toBe('done');
  });
});

gon 객체 조작

gon 또는 window.gon은 백엔드에서 데이터를 전달하는 데 사용되는 전역 객체입니다. 테스트가 해당 값에 의존하는 경우 직접 수정할 수 있습니다:

describe('로그인한 경우', () => {
  beforeEach(() => {
    gon.current_user_id = 1;
  });

  it('메시지가 표시되는지 확인', () => {
    expect(wrapper.text()).toBe('로그인됨!');
  });
})

gon은 테스트가 격리되도록 모든 테스트에서 재설정됩니다.

테스트 격리 여부 확인

테스트는 일반적으로 테스트되는 컴포넌트를 반복적으로 설정하는 패턴으로 설계됩니다. 이는 일반적으로 beforeEach 후크를 사용하여 달성됩니다.

예시

  let wrapper;

  beforeEach(() => {
    wrapper = mount(Component);
  });

enableAutoDestroy를 사용하면 wrapper.destroy()를 수동으로 호출할 필요가 없어집니다. 그러나 일부 모의, 스파이 및 픽스처는 다운되어야 하며, 이를 위해 afterEach 후크를 활용할 수 있습니다.

예시

  let wrapper;

  afterEach(() => {
    fakeApollo = null;
    store = null;
  });

로컬 전용 Apollo 쿼리 및 뮤테이션 테스트

백엔드에 추가되기 전에 새로운 쿼리 또는 뮤테이션을 추가하려면 @client 지시문을 사용할 수 있습니다. 예를 들어:

mutation setActiveBoardItemEE($boardItem: LocalBoardItem, $isIssue: Boolean = true) {
  setActiveBoardItem(boardItem: $boardItem) @client {
    ...Issue @include(if: $isIssue)
    ...EpicDetailed @skip(if: $isIssue)
  }
}

이러한 호출에 대한 테스트 케이스를 작성할 때, 리졸버를 사용하여 올바른 매개변수로 호출되었는지 확인할 수 있습니다.

예를 들어, 래퍼를 생성할 때 리졸버가 쿼리나 뮤테이션에 매핑되어 있는지 확인해야 합니다. 여기에서 우리가 모킹하는 뮤테이션은 setActiveBoardItem입니다:

const mockSetActiveBoardItemResolver = jest.fn();
const mockApollo = createMockApollo([], {
    Mutation: {
      setActiveBoardItem: mockSetActiveBoardItemResolver,
    },
});

다음 코드에서는 네 가지 인수를 전달해야 합니다. 두 번째는 모킹된 쿼리나 뮤테이션의 입력 변수 모음이어야 하며, 올바른 매개변수로 호출되었는지 테스트하기 위해 필요합니다:

it('닫을 때 setActiveBoardItemMutation을 호출함', async () => {
    wrapper.findComponent(GlDrawer).vm.$emit('close');

    await waitForPromises();

    expect(mockSetActiveBoardItemResolver).toHaveBeenCalledWith(
        {},
        {
            boardItem: null,
        },
        expect.anything(),
        expect.anything(),
    );
});

Jest 최상의 사례

기본 값 비교시 toEqual 대신 toBe를 선호

Jest에는 toBetoEqual 매처가 있습니다. toBe는 값 비교에 Object.is를 사용하므로 기본적으로(default) toEqual보다 빠릅니다. 후자는 복잡한 객체가 비교되어야 할 때만 사용되어야 합니다.

예시:

const foo = 1;

// 나쁨
expect(foo).toEqual(1);

// 좋음
expect(foo).toBe(1);

적합한 매처를 선호하세요

Jest는 테스트를 보다 가독성 있고 이해하기 쉬운 오류 메시지를 생성하도록 만들기 위해 toHaveLengthtoBeUndefined와 같은 유용한 매처를 제공합니다. 매처의 전체 목록을 확인하세요.

예시:

const arr = [1, 2];

// 출력:
// 예상된 길이: 1
// 받은 길이: 2
expect(arr).toHaveLength(1);

// 출력:
// 예상: 1
// 받은: 2
expect(arr.length).toBe(1);

// 출력:
// expect(received).toBe(expected) // Object.is equality
// 예상: undefined
// 받은: "bar"
const foo = 'bar';
expect(foo).toBe(undefined);

// 출력:
// expect(received).toBeUndefined()
// 받은: "bar"
const foo = 'bar';
expect(foo).toBeUndefined();

toBeTruthytoBeFalsy 사용을 피하세요

Jest는 또한 toBeTruthytoBeFalsy 매처를 제공합니다. 이들을 사용하지 말아야 합니다. 왜냐하면 이들은 테스트를 약화시키고 잘못된 긍정적인 결과를 만들기 때문입니다.

예를 들어, expect(someBoolean).toBeFalsy()someBoolean === null일 때 통과하며, someBoolean === false일 때도 통과합니다.

까다로운 toBeDefined 매처

Jest에는 까다로운 toBeDefined 매처가 있습니다. 이는 잘못된 긍정적인 테스트를 만들 수 있습니다. 왜냐하면 이는 주어진 값이 undefined인지만을 확인합니다.

// 나쁨: 만일 찾기가 null을 반환하면 테스트는 통과합니다
expect(wrapper.find('foo')).toBeDefined();

// 좋음
expect(wrapper.find('foo').exists()).toBe(true);

setImmediate 사용을 피하세요

setImmediate 사용을 피하세요. setImmediate은 I/O가 완료된 후에 콜백을 실행하기 위한 임시적인 해결책입니다. 이는 Web API의 일부가 아니며, 따라서 당사는 단위 테스트에서 NodeJS 환경을 대상으로 하고 있습니다.

setImmediate 대신에 보류 중인 타이머를 실행하기 위해 jest.runAllTimersjest.runOnlyPendingTimers를 사용하세요. 후자는 코드에 setInterval이 있는 경우 유용합니다. 기억하세요: 당사의 Jest 구성은 가짜 타이머를 사용합니다.

비결정적인 스펙 피하기

비결정적인 요소는 오작동하고 취약한 스펙의 둥지입니다. 이러한 스펙은 CI 파이프라인을 망가뜨리고 다른 기여자들의 작업 흐름을 방해하게 됩니다.

  1. 테스트 대상의 협력자들 (예: Axios, Apollo, Lodash 도우미 등)과 테스트 환경 (예: Date 등)이 시스템과 시간을 초과하여 일관되게 동작하는지 확인하세요.
  2. 테스트가 집중되어 있고 “추가 작업”을 수행하지 않는지 확인하세요 (예: 개별 테스트에서 테스트 대상을 불필요하게 여러 번 생성).

결정론적 날짜 생성을 위한 Date 가짜 사용

Date는 우리의 Jest 환경에서 기본적으로 가짜로 조작됩니다. 이는 Date()Date.now()의 모든 호출이 고정된 결정론적인 값을 반환함을 의미합니다.

기본 가짜 Date를 변경해야 한다면, describe 블록 내에서 useFakeDate를 호출할 수 있으며, 해당 describe 컨텍스트 내의 스펙에 대해 해당 날짜가 교체됩니다.

import { useFakeDate } from 'helpers/fake_date';

describe('cool/component', () => {
  // 기본 가짜 `Date`
  const TODAY = new Date();

  // 참고: `useFakeDate`는 테스트 실행 중에 호출될 수 없습니다(`it`, `beforeEach`, `beforeAll` 등 내부에서 호출될 수 없음).
  describe('애다 러블리스의 생일 때', () => {
    useFakeDate(1815, 11, 10)

    it('기본 날짜가 아님', () => {
      expect(new Date()).not.toEqual(TODAY);
    });
  });

  it('이 컨텍스트에서는 여전히 기본 날짜임', () => {
    expect(new Date()).toEqual(TODAY)
  });
})

마찬가지로, 실제 Date 클래스를 사용해야 하는 경우, 어떤 describe 블록에서든 useRealDate를 가져와 호출할 수 있습니다:

import { useRealDate } from 'helpers/fake_date';

// 주의: `useRealDate`는 테스트 실행 중에 호출될 수 없습니다(`it`, `beforeEach`, `beforeAll` 등 내부에서 호출될 수 없음).
describe('실제 날짜 사용시', () => {
  useRealDate();
});

결정론적 Math.random 생성을 위한 가짜 사용

테스트 대상이 이에 의존할 때 Math.random을 가짜로 바꿔보세요.

beforeEach(() => {
  // https://xkcd.com/221/
  jest.spyOn(Math, 'random').mockReturnValue(0.4);
});

팩토리

TBU

Jest로 모의 전략

스텁과 모의화

스텁 또는 스파이는 종종 동의어로 사용됩니다. Jest에서는 .spyOn 메서드 덕분에 사용하기가 매우 쉽습니다. 공식 문서 보다 어려운 부분은 함수나 종속 항목에 대해 사용할 수 있는 모의화인데, 이는 도전적일 수 있습니다.

수동 모듈 목업

수동 목업은 전체 Jest 환경에서 모듈을 모의(mock)화하는 데 사용됩니다. 이는 우리의 테스트 환경에서 쉽게 소비되지 않는 모듈을 모의화하여 단위 테스트를 단순화하는 매우 강력한 테스트 도구입니다.

경고: 특정 몇 개의 스펙에서만 필요한 경우 수동 목업을 일관되게 적용해서는 안 됩니다. 대신, 관련된 스펙 파일에서 jest.mock(..)를 사용하는 것을 고려해보세요.

수동 목업을 어디에 두어야 하나요?

Jest는 소스 모듈 옆에 __mocks__/ 디렉터리에 목업을 배치함으로써 수동 모듈 목업을 지원합니다 (예: app/assets/javascripts/ide/__mocks__). 이렇게 하지 마십시오. 우리는 모든 테스트 관련 코드를 한 곳인 spec/ 폴더에 유지하려고 합니다.

node_modules 패키지에 수동 목업이 필요한 경우 spec/frontend/__mocks__ 폴더를 사용하세요. 여기에는 패키지 monaco-editor의 Jest 목업의 예가 있습니다.

CE 모듈에 수동 목업이 필요한 경우 구현을 spec/frontend/__helpers__/mocks에 배치하고 frontend/test_setup (또는 frontend/shared_test_setup)에 다음과 유사한 줄을 추가하세요:

// "~/lib/utils/axios_utils"은 실제 모듈의 경로입니다
// "helpers/mocks/axios_utils"은 모킹된 구현의 경로입니다
jest.mock('~/lib/utils/axios_utils', () => jest.requireActual('helpers/mocks/axios_utils'));

수동 목업 예시

  • __helpers__/mocks/axios_utils - 이 목업은 테스트를 통과시키지 않기 위해 어떠한 모킹되지 않은 요청도 통과시키고 싶지 않기 때문에 유용합니다. 또한 axios.waitForAll과 같은 테스트 헬퍼를 주입할 수 있습니다.
  • __mocks__/mousetrap/index.js - 이 목업은 모듈 자체가 AMD 형식을 사용하여 webpack에서 이해하지만 jest 환경과 호환되지 않기 때문에 유용합니다. 이 목업은 어떠한 동작도 제거하지 않고, 단지 좋은 es6 호환 래퍼를 제공합니다.
  • __mocks__/monaco-editor/index.js - 이 목업은 Monaco 패키지가 Jest 환경에서 완전히 호환되지 않기 때문에 유용합니다. 실제로 webpack는 비정상적으로 작동시키기 위해 특별한 로더가 필요합니다. 이 목업은 Jest에서 이 패키지를 사용할 수 있게 합니다.

목업을 가볍게 유지하세요

전역 목업은 마법을 부리고 기술적으로 테스트 커버리지를 줄일 수 있습니다. 목업화가 이득이 있다고 판단될 때:

  • 목업을 간결하고 집중시킵니다.
  • 목업의 최상위 주석에 왜 필요한지 설명합니다.

추가적인 목업 기술

사용 가능한 목업 기능의 전체 개요는 공식 Jest 문서를 참조하세요.

프론트엔드 테스트 실행

픽스처를 생성하기 전에 실행 중인 GDK 인스턴스가 있는지 확인하세요.

프론트엔드 테스트를 실행하려면 다음 명령을 사용하세요:

  • rake frontend:fixtures픽스처을 (재)생성합니다. 테스트를 실행하기 전에 픽스처가 최신 상태인지 확인하세요.
  • yarn jest는 Jest 테스트를 실행합니다.

실시간 테스트 및 집중 테스트 – Jest

테스트 스위트를 작업할 때, 이 스펙을 워치 모드에서 실행하여 저장할 때마다 자동으로 다시 실행할 수 있습니다.

# Watch 및 이름이 icon인 모든 스펙 다시 실행
yarn jest --watch icon

# 특정 파일을 워치하고 다시 실행
yarn jest --watch path/to/spec/file.spec.js

또한 --watch 플래그 없이 일부 집중 테스트를 실행할 수도 있습니다.

# 특정 jest 파일 실행
yarn jest ./path/to/local_spec.js
# 특정 jest 폴더 실행
yarn jest ./path/to/folder/
# 경로에 용어를 포함하는 모든 jest 파일 실행
yarn jest term

프론트엔드 테스트 픽스처

프론트엔드 픽스처는 백엔드 컨트롤러로부터의 응답을 담고 있는 파일입니다. 이러한 응답은 HAML 템플릿에서 생성된 HTML이거나 JSON 페이로드일 수 있습니다. 이러한 응답을 사용하는 프론트엔드 테스트는 종종 픽스처를 사용하여 백엔드 코드와의 정확한 통합을 검증합니다.

픽스처 사용

JSON 또는 HTML 픽스처를 가져오려면 test_fixtures 별칭을 사용하여 import합니다.

import responseBody from 'test_fixtures/some/fixture.json' // tmp/tests/frontend/fixtures-ee/some/fixture.json을 로드합니다.

it('요청을 수행합니다', () => {
  axiosMock.onGet(endpoint).reply(200, responseBody);

  myButton.click();

  // ...
});

픽스처 생성

다음 위치에서 테스트 픽스처를 생성하는 코드를 찾을 수 있습니다:

  • CE에서 테스트를 실행하는 경우: spec/frontend/fixtures/
  • EE에서 테스트를 실행하는 경우: ee/spec/frontend/fixtures/

다음을 실행하여 픽스처를 생성할 수 있습니다:

  • bin/rake frontend:fixtures를 실행하여 모든 픽스처를 생성합니다.
  • bin/rspec spec/frontend/fixtures/merge_requests.rb를 실행하여 특정 픽스처를 생성합니다 (이 경우 merge_request.rb에 대한 픽스처를 생성합니다).

생성된 픽스처는 tmp/tests/frontend/fixtures-ee에 있습니다.

픽스처 다운로드

우리는 GitLab CI에서 픽스처를 생성하고 이를 패키지 레지스트리에 저장합니다.

scripts/frontend/download_fixtures.sh 스크립트는 이러한 픽스처를 로컬에서 다운로드하고 추출하는 데 사용됩니다.

# 로컬 브랜치의 커밋을 확인하여 gitlab-org/gitlab 상에 프론트엔드 픽스처 패키지가 있는지 확인합니다.
#
# 패키지가 있는 경우, 다운로드하고 추출합니다
$ scripts/frontend/download_fixtures.sh

# 마지막 10개 커밋만 확인하며 현재 체크아웃된 브랜치에서 수행
$ scripts/frontend/download_fixtures.sh --max-commits=10

# 현재 체크아웃된 브랜치 대신 로컬 master 브랜치의 커밋을 확인
$ scripts/frontend/download_fixtures.sh --branch master

새로운 픽스처 생성

각 픽스처에 대해 출력 파일의 response 변수 내용을 찾을 수 있습니다. 예를 들어, spec/frontend/fixtures/merge_requests.rb의 이름이 "merge_requests/diff_discussion.json"인 테스트는 tmp/tests/frontend/fixtures-ee/merge_requests/diff_discussion.json에 대한 출력 파일을 생성합니다. response 변수는 테스트가 type: :request 또는 type: :controller로 표시된 경우 자동으로 설정됩니다.

새로운 픽스처를 생성할 때는 spec/controllers/ 또는 spec/requests/의 해당 엔드포인트에 대한 테스트를 확인하는 것이 좋습니다.

GraphQL 쿼리 픽스처

get_graphql_query_as_string 도우미 메서드를 사용하여 GraphQL 쿼리의 결과를 나타내는 픽스처를 만들 수 있습니다. 예를 들어:

# spec/frontend/fixtures/releases.rb

describe GraphQL::Query, type: :request do
  include GraphqlHelpers

  all_releases_query_path = 'releases/graphql/queries/all_releases.query.graphql'

  it "graphql/#{all_releases_query_path}.json" do
    query = get_graphql_query_as_string(all_releases_query_path)

    post_graphql(query, current_user: admin, variables: { fullPath: project.full_path })

    expect_graphql_errors_to_be_empty
  end
end

이렇게 하면 tmp/tests/frontend/fixtures-ee/graphql/releases/graphql/queries/all_releases.query.graphql.json에 위치한 새로운 픽스처가 생성됩니다.

Jest 테스트에서는 test_fixtures 별칭을 사용하여 JSON 픽스처를 가져올 수 있습니다 이전에 설명한 대로.

데이터 기반 테스트

RSpec의 매개변수화된 테스트와 유사하게, Jest는 다음을 위한 데이터 기반 테스트를 지원합니다:

  • test.each(it.each의 별칭)를 사용하여 개별 테스트
  • describe.each를 사용하여 테스트 그룹

이는 테스트 내에서의 반복을 줄이는 데 유용할 수 있습니다. 각 옵션은 데이터 값을 배열 또는 태그드 템플릿 리터럴로 사용할 수 있습니다.

예를 들어:

// 테스트할 함수
const icon = status => status ? 'pipeline-passed' : 'pipeline-failed'
const message = status => status ? 'pipeline-passed' : 'pipeline-failed'

// 배열 블록을 사용한 테스트
it.each([
    [false, 'pipeline-failed'],
    [true, 'pipeline-passed']
])('아이콘은 %s를 반환합니다 %s',
 (status, icon) => {
    expect(renderPipeline(status)).toEqual(icon)
 }
);

참고: 템플릿 리터럴 블록은 스펙 출력에 예쁘게 출력이 필요하지 않은 경우에만 사용합니다. 예를 들어, 빈 문자열, 중첩된 객체 등에 대해서는 옵션에 예쁘게 출력이 필요한 경우에는 배열 블록 구문 사용을 선호합니다. 그런 후, 스펙 출력에서 빈 문자열('')과 비어 있지 않은 문자열('search string') 사이의 차이가 보이게 됩니다. 그에 비해, 템플릿 리터럴 블록을 사용하면 빈 문자열은 공백으로 표시되어 혼란스러운 개발 경험이 될 수 있습니다.

// 좋지 않은 방법
it.each`
    searchTerm | expected
    ${''} | ${{ issue: { users: { nodes: [] } } }}
    ${'search term'} | ${{ issue: { other: { nested: [] } } }}
`('검색어가 $searchTerm인 경우, $expected를 반환합니다', ({ searchTerm, expected }) => {
  expect(search(searchTerm)).toEqual(expected)
});

// 좋은 방법
it.each([
    ['', { issue: { users: { nodes: [] } } }],
    ['search term', { issue: { other: { nested: [] } } }],
])('검색어가 %p인 경우, %p를 반환할 것으로 예상합니다',
 (searchTerm, expected) => {
    expect(search(searchTerm)).toEqual(expected)
 }
);

// 태그드 템플릿 리터럴 블록을 사용하는 테스트 스위트
describe.each`
    status   | icon                 | message
    ${false} | ${'pipeline-failed'} | ${'Pipeline failed - boo-urns'}
    ${true}  | ${'pipeline-passed'} | ${'Pipeline succeeded - win!'}
`('파이프라인 구성요소', ({ status, icon, message }) => {
    it(`아이콘 ${icon}은 상태 ${status}와 함께 반환됩니다`, () => {
        expect(icon(status)).toEqual(message)
    })

    it(`메시지 ${message}은 상태 ${status}와 함께 반환됩니다`, () => {
        expect(message(status)).toEqual(message)
    })
});

주의 사항

JavaScript로 인한 RSpec 오류

기본적으로 RSpec 단위 테스트는 headless 브라우저에서 JavaScript를 실행하지 않고, rails에서 생성된 HTML을 검사하는 것에 의존합니다.

통합 테스트가 올바르게 실행되려면 JavaScript에 의존하는 경우, 테스트가 실행될 때 스펙이 JavaScript를 활성화하도록 구성해야 합니다. 이를 하지 않으면 스펙 실행기가 모호한 오류 메시지를 표시할 수 있습니다.

RSpec 테스트에서 JavaScript 드라이버를 활성화하려면 개별 스펙이나 JavaScript가 필요한 여러 스펙을 포함하는 컨텍스트 블록에 :js를 추가하세요.

# 하나의 스펙에 대해
it '학대 신고에 관한 정보 표시', :js do
  # 어서션...
end

describe "Admin::AbuseReports", :js do
  it '학대 신고에 관한 정보 표시' do
    # 어서션...
  end
  it '학대 신고 추가 버튼 표시' do
    # 어서션...
  end
end

비동기 가져오기로 인한 Jest 테스트 시간 초과

모듈이 실행 시점에서 비동기적으로 다른 모듈을 가져오는 경우, 이러한 모듈은 Jest 로더에 의해 실행 시점에서 변환되어야 합니다. 이로 인해 Jest 시간 초과가 발생할 수 있습니다.

만약 이 문제에 직면한다면, Jest가 컴파일 시간에 모듈을 컴파일하고 캐싱하도록 모듈을 이전에 가져오는 것을 고려하세요.

다음 예를 살펴보세요:

// the_subject.js

export default {
  components: {
    // 크고 항상 필요하지 않은 경우 비동기 가져오기를 위해 Thing 가져오기
    Thing: () => import(/* webpackChunkName: 'thing' */ './path/to/thing.vue'),
  }
};

Jest는 thing.vue 모듈을 자동으로 변환하지 않으며, 그 크기에 따라 Jest가 시간 초과될 수 있습니다. 이를 해결하기 위해 다음과 같이 모듈을 이전에 가져오도록 할 수 있습니다:

// the_subject_spec.js

import Subject from '~/feature/the_subject.vue';

// Jest가 변환하고 캐시하도록 강제로 가져오기
// eslint-disable-next-line no-unused-vars
import _Thing from '~/feature/path/to/thing.vue';

참고: 테스트 시간 초과를 무시하지 마십시오. 이는 실제로 프로덕션 문제가 있을 수 있는 신호일 수 있습니다. 이 기회를 활용하여 프로덕션 웹팩 번들 및 청크를 분석하고, 비동기적으로 가져오기에 프로덕션 문제가 없는지 확인하세요.

프런트엔드 테스트 레벨 개요

프런트엔드 테스트 레벨에 대한 주요 정보는 테스트 레벨 페이지에서 찾을 수 있습니다.

프런트엔드 개발에 관련된 테스트는 다음 위치에서 찾을 수 있습니다:

  • spec/frontend/, Jest 테스트용
  • spec/features/, RSpec 테스트용

RSpec는 완전한 기능 테스트를 실행하며, Jest 디렉토리에는 프런트엔드 단위 테스트, 프런트엔드 컴포넌트 테스트, 그리고 프런트엔드 통합 테스트가 포함되어 있습니다.

2018년 5월 이전에는 features/에 Spinach에서 실행되는 기능 테스트가 포함되어 있었습니다. 이러한 테스트는 2018년 5월에 코드베이스에서 제거되었습니다 (#23036).

또한 Vue 컴포넌트 테스트 노트도 참고하세요.

테스트 도우미

테스트 도우미는 spec/frontend/__helpers__에서 찾을 수 있습니다. 새 도우미를 도입한다면 해당 디렉토리에 배치하세요.

Vuex 도우미: testAction

테스트 동안 작업을 보다 간편하게 만들기 위해 사용 가능한 헬퍼가 있습니다. 공식 문서에 따르면 다음과 같이 사용하세요:

// 이와 같이 사용하는 것을 권장하며, 단일 객체 인수를 사용하여 테스트를 읽을 때 매개변수가 명확해집니다
await testAction({
  action: actions.actionName,
  payload: { deleteListId: 1 },
  state: { lists: [1, 2, 3] },
  expectedMutations: [ { type: types.MUTATION} ],
  expectedActions: [],
});

// 새 테스트에 대해 이전 방법을 사용하지 마세요
testAction(
  actions.actionName, // 액션
  { }, // 액션에 전달될 매개변수
  state, // 상태
  [
    { type: types.MUTATION},
    { type: types.MUTATION_1, payload: {}},
  ], // 커밋된 변형
  [
    { type: 'actionName', payload: {}},
    { type: 'actionName1', payload: {}},
  ] // 전달된 액션
  done,
);

Axios 요청 완료까지 대기

spec/frontend/__helpers__/mocks/axios_utils.js에 위치한 Axios Utils 모의 모듈에는 HTTP 요청을 생성하는 Jest 테스트를 위한 두 가지 도우미 메서드가 포함되어 있습니다. 예를 들어, Vue 컴포넌트가 라이프 사이클의 일부로 요청을 하는 경우에 매우 유용합니다.

  • waitFor(url, callback): url로의 요청이 완료된 후 callback을 실행합니다(성공적이었건 실패적이었건).
  • waitForAll(callback): 보류 중인 모든 요청이 완료되면 callback을 실행합니다. 보류 중인 요청이 없는 경우, 다음 틱에서 callback을 실행합니다.

이 두 함수는 요청이 완료된 후 callback을 다음 틱에서 실행합니다(setImmediate()를 사용해서), .then() 또는 .catch() 핸들러가 실행될 수 있도록 합니다.

shallowMountExtendedmountExtended

shallowMountExtendedmountExtended 유틸리티는 이용 가능한 DOM 테스팅 라이브러리 쿼리find 또는 findAll로 접두사를 붙여 사용할 수 있도록 합니다.

import { shallowMountExtended } from 'helpers/vue_test_utils_helper';

describe('FooComponent', () => {
  const wrapper = shallowMountExtended({
    template: `
      <div data-testid="gitlab-frontend-stack">
        <p>GitLab 프론트엔드 스택</p>
        <div role="tablist">
          <button role="tab" aria-selected="true">Vue.js</button>
          <button role="tab" aria-selected="false">GraphQL</button>
          <button role="tab" aria-selected="false">SCSS</button>
        </div>
      </div>
    `,
  });

  it('`findByTestId`로 요소를 찾습니다', () => {
    expect(wrapper.findByTestId('gitlab-frontend-stack').exists()).toBe(true);
  });

  it('`findByText`로 요소를 찾습니다', () => {
    expect(wrapper.findByText('GitLab 프론트엔드 스택').exists()).toBe(true);
    expect(wrapper.findByText('TypeScript').exists()).toBe(false);
  });

  it('`findAllByRole`로 요소를 찾습니다', () => {
    expect(wrapper.findAllByRole('tab').length).toBe(3);
  });
});

spec/frontend/alert_management/components/alert_details_spec.js에서 예제를 확인하세요.

오래된 브라우저에서의 테스트

일부 회귀는 특정 브라우저 버전에만 영향을 미칠 수 있습니다. 특정 브라우저에서 Firefox 또는 BrowserStack을 사용하여 다음 단계를 따라 설치하고 테스트할 수 있습니다:

BrowserStack

BrowserStack을 사용하면 1200개 이상의 모바일 장치 및 브라우저에서 테스트할 수 있습니다. 라이브 앱을 통해 직접 사용하거나 크롬 익스텐션을 설치하여 쉽게 액세스할 수 있습니다. GitLab의 Engineering 보안 금고에 저장된 자격 증명으로 BrowserStack에 로그인할 수 있습니다. GitLab 공유 1Password 계정에서 저장된 자격 증명을 사용하세요.

Firefox

macOS

릴리스 FTP 서버 https://ftp.mozilla.org/pub/firefox/releases/에서 Firefox의 이전 버전을 다운로드할 수 있습니다.

  1. 웹 사이트에서 이 경우 50.0.1처럼 버전을 선택합니다.
  2. mac 폴더로 이동합니다.
  3. 선호하는 언어를 선택합니다. DMG 파일이 안에 있습니다. 다운로드합니다.
  4. 응용 프로그램을 Applications 폴더가 아닌 다른 폴더로 드래그 앤 드롭합니다.
  5. 응용 프로그램의 이름을 Firefox_Old와 같은 이름으로 변경합니다.
  6. 응용 프로그램을 Applications 폴더로 이동합니다.
  7. 터미널을 열고 /Applications/Firefox_Old.app/Contents/MacOS/firefox-bin -profilemanager를 실행하여 해당 Firefox 버전에 특화된 새 프로필을 만듭니다.
  8. 프로필을 만든 후 앱을 종료하고 일반적으로 실행합니다. 이제 작동하는 이전 Firefox 버전을 사용할 수 있습니다.

스냅샷

Jest 스냅샷 테스트는 특정 구성 요소의 HTML 출력에 예상치 못한 변경을 방지하는 유용한 방법입니다. 이러한 테스트는 다른 테스트 방법(예: vue-tests-utils를 사용하여 요소를 확인하는 것)으로 요구되는 사용 사례를 다루지 못할 때에만 사용해야 합니다. GitLab에서 사용하기 위해 몇 가지 강조해야 할 지침이 있습니다:

  • 스냅샷을 코드로 취급합니다.
  • 스냅샷 파일을 블랙박스로 간주하지 마십시오.
  • 스냅샷의 출력에 유의하십시오. 그렇지 않으면 실제 가치가 제공되지 않습니다. 일반적으로 생성된 스냅샷 파일을 다른 코드와 마찬가지로 읽어야 합니다.

스냅샷 테스트를 단순히 테스트 대상 항목에 넣은 날 것의 String 표현을 저장하는 간단한 방법으로 생각하세요. 이것은 구성 요소, 스토어, 생성된 출력의 복잡한 부분 등의 변경을 평가하는 데 사용할 수 있습니다. 몇 가지 권장 사항 Do's and Don'ts을 보려면 아래 목록을 확인하세요. 스냅샷 테스트는 매우 강력한 도구일 수 있지만, 단위 테스트를 대체하는 것이 아니라 보완하는 목적으로 사용되어야 합니다.

Jest는 스냅샷을 만들 때 염두에 둘 일련의 모범 사례를 제공합니다.

장단점

장점

  • 중요한 HTML 구조의 실수로 인한 변경에 대해 좋은 경고를 제공합니다.
  • 설치의 용이성

단점

  • vue-tests-utils가 제공하는 요소 발견 및 직접 존재 여부를 단언함으로써 명확성이나 가드 레일을 부족하게 합니다.
  • 의도적으로 구성 요소를 업데이트할 때 불필요한 잡음을 만듭니다.
  • 버그의 스냅샷을 찍는 고위험으로, 이후 문제를 해결할 때 테스트가 실패하게 됩니다.
  • 이해하기 어렵거나 대체하기 어려운 의미 있는 단언문이나 기대치가 스냅샷에 없어서 이해하기 어렵습니다.
  • GitLab UI와 같은 의존성과 함께 사용할 경우, 테스트의 취약성을 초래하여 테스트하려는 컴포넌트의 HTML이 하부 라이브러리에 의해 변경됩니다.

사용 시기

스냅샷을 사용할 때

  • 우연한 변경으로 인해 중요한 HTML 구조가 변경되지 않도록 보호하기 위해
  • 복잡한 유틸리티 함수의 JS 객체 또는 JSON 출력을 단언하기 위해

사용하지 않아야 할 때

스냅샷을 사용하지 말아야 할 때

  • 테스트를 vue-tests-utils를 사용하여 작성할 수 있는 경우
  • 컴포넌트의 논리를 단언하는 경우
  • 데이터 구조의 출력을 예측하는 경우
  • 리포지토리 외부의 UI 요소가 있는 경우(GitLab UI 버전 업데이트를 생각해보세요)

예시

일반적으로 스냅샷 테스트의 단점이 장점을 상회합니다. 이를 더 잘 설명하기 위해, 이 섹션에서는 스냅샷 테스트를 사용하려는 유혹을 느낄 수 있는 상황 몇 가지와 그들이 좋은 패턴이 아니라는 이유에 대한 몇 가지 예시를 보여줍니다.

예시 #1 - 요소 가시성

요소 가시성을 테스트할 때는, 주어진 구성 요소를 찾고 VTU(wrapper)에서 기본 .exists() 메서드를 호출하는 것이 더 읽기 쉽고 견고한 테스트를 제공합니다. 아래 예시를 살펴보면, 스냅샷에 대한 단언이 원하는 내용을 알려주지 않는다는 것을 알 수 있습니다. 테스트가 어떤 동작을 기대하는지 확인하는 데는 it 설명만 의존하고 스냅샷이 원하는 동작을 캡처했다는 가정에만 의존합니다.

<template>
  <my-component v-if="isVisible" />
</template>

나쁨:

it('컴포넌트를 숨깁니다', () => {
  createComponent({ props: { isVisible: false }})

  expect(wrapper.element).toMatchSnapshot()
})

it('컴포넌트를 보여줍니다', () => {
  createComponent({ props: { isVisible: true }})

  expect(wrapper.element).toMatchSnapshot()
})

좋음:

it('컴포넌트를 숨깁니다', () => {
  createComponent({ props: { isVisible: false }})

  expect(findMyComponent().exists()).toBe(false)
})

it('컴포넌트를 보여줍니다', () => {
  createComponent({ props: { isVisible: true }})

  expect(findMyComponent().exists()).toBe(true)
})

또한, 잘못된 prop을 컴포넌트에 전달하여 잘못된 가시성을 가진 경우를 상상해보세요: 스냅샷 테스트는 여전히 통과하게 될 것이며, 그 결과 테스트가 고장났다는 것을 스냅샷의 출력을 다시 확인하지 않는 한 알 수 없게 됩니다.

예시 #2 - 텍스트의 존재

구성 요소 내에서 텍스트를 찾는 것은 vue-test-utils 방법 wrapper.text()를 사용하여 매우 쉽습니다. 그러나 형식이나 HTML 중첩으로 인해 반환된 값이 일관되지 않은 공백을 가질 때 스냅샷을 사용하기가 유혹스러울 수 있는 경우가 있습니다.

이러한 경우에는 각 문자열을 개별적으로 단언하고 여러 번의 단언을 하는 것이 좋습니다. 이는 DOM 레이아웃에 대한 변경이 있으면 텍스트가 여전히 완벽하게 형식화되어 있더라도 스냅샷 테스트가 실패하기 때문입니다.

<template>
  <gl-sprintf :message="my-message">
    <template #code="{ content }">
      <code>{{ content }}</code>
    </template>
  </gl-sprintf>
  <p> My second message </p>
</template>

나쁨:

it('내가 기대하는 대로 텍스트를 렌더링합니다', () => {
  expect(wrapper.text()).toMatchSnapshot()
})

좋음:

it('코드 스니펫을 렌더링합니다', () => {
  expect(findCodeTag().text()).toContain("myFunction()")
})

it('문단 텍스트를 렌더링합니다', () => {
  expect(findOtherText().text()).toBe("My second message")
})

예시 #3 - 복잡한 HTML

매우 복잡한 HTML이 있는 경우에는, 전체적으로 캡처하는 대신 구체적이고 중요한 지점을 단언하는 것에 집중해야 합니다. 스냅샷 테스트의 가치는 개발자에게 의도하지 않은 HTML 구조를 실수로 변경했을 수 있다는 것을 알려주는 것입니다. 변경의 결과가 읽기 어렵다면, 즉 복잡한 HTML 출력의 경우에는, 변경 신호 그 자체가 충분한가요? 그렇다면 스냅샷 없이도 달성할 수 있나요?

복잡한 HTML 출력의 좋은 예는 GlTable입니다. 스냅샷 테스트는 행과 열 구조를 캡처할 수 있기 때문에 유용해 보일 수 있지만, 텍스트를 단언하거나 행과 열의 수를 수동으로 세는 것이 좋습니다.

<template>
  <gl-table ...all-them-props />
</template>

나쁨:

it('내가 기대하는 대로 GlTable을 렌더링합니다', () => {
  expect(findGlTable().element).toMatchSnapshot()
})

좋음:

it('올바른 행 수를 렌더링합니다', () => {
  expect(findGlTable().findAllRows()).toHaveLength(expectedLength)
})

it('만월에만 나타나는 특별한 아이콘을 렌더링합니다', () => {
  expect(findGlTable().findMoonIcon().exists()).toBe(true)
})

it('올바른 이메일 형식을 렌더링합니다', () => {
  expect(findGlTable().text()).toContain('my_strange_email@shaddyprovide.com')
})

더 많은 양의 내용이지만, 이제 이제 테스트가 내부 구현을 변경하더라도 깨지지 않을 것이며, 리팩터링하거나 테이블에 추가할 때 보존해야 하는 중요한 정보를 다른 개발자(또는 6개월 후의 자신)에게 전달할 수 있습니다.

스냅샷 찍는 방법

it('makes the name look pretty', () => {
  expect(prettifyName('Homer Simpson')).toMatchSnapshot()
})

이 테스트가 처음 실행될 때 새로운 .snap 파일이 생성됩니다. 다음과 같이 보일 것입니다.

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`makes the name look pretty`] = `
Sir Homer Simpson the Third
`

이제 이 테스트를 호출할 때마다 새로운 스냅샷이 이전에 생성된 버전과 평가될 것입니다. 스냅샷 파일의 내용을 이해하고 주의 깊게 다루는 것이 중요함을 강조하기 위해입니다. 스냅샷은 스냅샷의 출력이 읽기에 너무 복잡하거나 크면 가치가 상실될 수 있습니다. 이는 스냅샷을 인간이 읽을 수 있는 항목에만 유지하고, 머지 요청 리뷰에서 평가할 수 있거나 절대 변하지 않을 것으로 보장되는 것으로 유지하는 것을 의미합니다. wrapperselements에 대해서도 동일하게 적용될 수 있습니다.

it('renders the component correctly', () => {
  expect(wrapper).toMatchSnapshot()
  expect(wrapper.element).toMatchSnapshot();
})

위의 테스트는 두 개의 스냅샷을 생성합니다. 코드베이스 안전성을 위해 어떤 스냅샷이 더 가치 있는지 결정하는 것이 중요합니다. 즉, 이러한 스냅샷 중 하나가 변경된다면 코드베이스에 잠재적인 문제가 있음을 강조하는 것이 도움이 될 수 있습니다. 이것은 우리의 지식 없이 기존 종속성이 변경될 때 예기치 않은 변경을 잡아낼 수 있습니다.

기능 테스트 시작하기

기능 테스트란

기능 테스트화이트박스 테스트로도 알려진 테스트로 브라우저를 생성하고 Capybara 도우미를 가집니다. 즉, 테스트는 다음과 같은 작업을 수행할 수 있습니다.

  • 브라우저에서 요소를 찾습니다.
  • 해당 요소를 클릭합니다.
  • API를 호출합니다.

기능 테스트는 실행 비용이 많이 듭니다. 이 유형의 테스트를 실행하기 전에 정말 그렇게 하고 싶은지 확인해야 합니다.

우리의 모든 기능 테스트는 Ruby로 작성되지만 종종 JavaScript 엔지니어에 의해 작성됩니다. 따라서, 다음 섹션에서는 Ruby 또는 Capybara에 대한 사전 지식이 없다는 전제하에 이러한 테스트를 언제, 어떻게 사용해야 하는지에 대한 명확한 지침을 제시합니다.

기능 테스트 사용 시기

이 테스트가 여러 구성 요소를 걸쳐 있는 경우, 사용자가 여러 페이지를 탐색해야 하는 경우, 양식을 제출하고 다른 곳에서 결과를 관찰해야 하는 경우, 단위 테스트로 수행할 경우 가짜 데이터와 구성 요소로 대량의 모의와 스텁이 필요한 경우에 기능 테스트를 사용해야 합니다.

기능 테스트는 특히 다음을 테스트할 때 유용합니다:

  • 여러 구성 요소가 성공적으로 함께 작동하는지.
  • 복잡한 API 상호 작용. 기능 테스트는 API와 상호 작용하기 때문에 느리지만 모의나 픽스처의 수준이 필요하지 않습니다.

기능 테스트 사용 금지 시기

당신이 이러한 방법으로 동일한 테스트 결과를 얻을 수 있는 경우, 기능 테스트 대신에 jestvue-test-utils 단위 테스트를 사용해야 합니다. 기능 테스트 실행 비용이 매우 큽니다.

같은 테스트 결과를 얻을 수 있는 경우에:

  • 당신이 구현하고 있는 동작이 단일 구성 요소에 모두 포함되어 있을 경우.
  • 원하는 효과를 유도하기 위해 다른 구성 요소의 동작을 시뮬레이트할 수 있을 경우.
  • 가상 DOM에서 UI 요소를 선택하여 원하는 효과를 유도.

이미 선택된 UI 요소를 트리거하기 위해 유닛 테스트를 사용할 수 있습니다.

또한 새로운 코드의 동작이 여러 구성 요소가 함께 작동해야 하는 경우, 구성 요소 트리의 더 높은 위치에서 동작을 테스트하는 것을 고려해야 합니다. 예를 들어, 우리가 코드가 ParentComponent라는 구성 요소에 있는 경우:

  <script>
  export default{
    name: ParentComponent,
    data(){
      return {
        internalData: 'oldValue'
      }
    },
     methods:{
      changeSomeInternalData(newVal){
        this.internalData = newVal
      }
     }
  }
  </script>
  <template>
   <div>
    <child-component-1 @child-event="changeSomeInternalData" />
    <child-component-2 :parent-data="internalData" />
   </div>
  </template>

이 예제에서:

  • ChildComponent1이 이벤트를 발생시킵니다.
  • ParentComponentinternalData 값을 변경합니다.
  • ParentComponent는 속성을 childComponent2로 전달합니다.

구성 요소 트리에 더 높은 위치에서 단위 테스트를 사용할 수 있습니다:

  • ParentComponent 내부 유닛 테스트 파일에서 childComponent1에서 예상 이벤트를 발생시킨다.
  • 속성이 childComponent2로 전달되는 것을 확인합니다.

그리고 각각의 자식 구성 요소는 이벤트가 발생했을 때와 속성이 변경되었을 때의 동작을 테스트합니다.

이 예는 더 큰 규모와 더 깊은 구성 요소 트리에서도 적용될 수 있습니다. 가상 DOM에서 확신을 갖고 구성 요소를 마운트하고 이벤트를 발생시키거나 요소를 선택할 수 있다면, 기능 테스트의 추가 비용을 피하고 단위 테스트를 사용하는 것이 확실히 가치가 있습니다.

테스트 생성 위치

기능 테스트는 spec/features 폴더에 위치합니다. 추가하려는 기능에 대한 페이지를 테스트할 수 있는 기존 파일을 찾아야 합니다. 예를 들어, 파이프라인 페이지에 새로운 기능 테스트를 추가하려는 경우 spec/features/projects/pipelines에서 원하는 테스트 파일을 찾을 수 있습니다.

기능 테스트 실행 방법

  1. 작동하는 GDK 환경이 있는지 확인합니다.
  2. gdk start 명령어로 gdk 환경을 시작합니다.
  3. 터미널에서 다음을 실행합니다:
   bundle exec rspec path/to/file:line_of_my_test

이 명령 앞에 WEBDRIVER_HEADLESS=0을 붙여 실제 브라우저를 여는 것으로 테스트를 실행할 수도 있습니다. 테스트 중에 컴퓨터에서 실제로 볼 수 있는 브라우저가 열리므로 디버깅에 매우 유용합니다.

테스트 작성 방법

기본 파일 구조

  1. 모든 문자열 리터럴을 변경할 수 없도록 만듭니다.

모든 기능 테스트에서 맨 처음 줄은 다음과 같아야 합니다:

  # frozen_string_literal: true

이는 모든 문자열 리터럴을 변경할 수 없도록 만듭니다. 성능상의 이점도 있지만 이 내용은 이 섹션의 범위를 벗어납니다.

  1. 종속 항목을 가져옵니다.

필요한 모듈을 가져와야 합니다. 대부분의 경우 spec_helper를 요구해야 합니다:

  require 'spec_helper'

다른 관련 모듈을 가져옵니다.

  1. RSpec가 우리의 테스트를 정의하기 위해 전역 범위를 만듭니다. 마치 jest에서 처음 describe 블록을 사용하는 것과 같습니다.

그런 다음, 맨 처음 RSpec 범위를 만들어야 합니다.

RSpec.describe 'Pipeline', :js do
  ...
end

하지만 이것과 다른 점은 루비의 모든 것처럼 이것이 실제로 class라는 것입니다. 즉, 맨 위에서 테스트에 필요한 모듈을 include할 수 있다는 것입니다. 예를 들어, RoutesHelpers를 포함할 수 있습니다.

RSpec.describe 'Pipeline', :js do
  include RoutesHelpers
  ...
end

모든 이 구현이 끝나면 파일은 다음과 같이 보일 것입니다:

# frozen_string_literal: true

require 'spec_helper'

RSpec.describe 'Pipeline', :js do
  include RoutesHelpers
end

데이터 시딩

각 테스트는 고유의 환경에 있으므로 필요한 데이터를 씨드해야 합니다. 예를 들어, “파이프라인” 예제를 계속 진행하려고 합니다. 루트(/namespace/project/-/pipelines/:id/)에 위치한 주요 파이프라인 페이지로 이동하는 테스트를 원한다고 가정해 봅시다.

대부분의 기능 테스트는 최소한 사용자를 만들어야 하므로, 로그인해야 합니다. 이 단계를 건너 뛸 수도 있지만, 일반적인 규칙으로는 특정한 익명 사용자가 본 기능을 명확히 보는 것을 목적으로 특별히 테스트하는 경우를 제외하고는 항상 사용자를 생성해야 합니다. 이는 테스트에서 설정한 수준의 권한을 명시적으로 확실히 하여 섹션이 변경될 때 변경하거나 새로운 권한 수준을 테스트하기 위해 필요합니다. 사용자를 만들려면 다음과 같이 할 수 있습니다:

  let(:user) { create(:user) }

이렇게 새로 만든 사용자를 포함하는 변수를 만들고, spec_helper를 가져왔으므로 create를 사용할 수 있습니다.

하지만 이 사용자를 사용하지는 않았기 때문에 변수일 뿐입니다. 따라서 테스트의 before do 블록에서 사용자로 로그인하여 각 테스트가 인증된 사용자로 시작하도록 할 수 있습니다.

  let(:user) { create(:user) }

  before do
    sign_in(user)
  end

이제 사용자가 있으므로 파이프라인 페이지에서 어떤 다른 것들이 필요한지 살펴보아야 합니다. /namespace/project/-/pipelines/:id/ 루트를 살펴보면 프로젝트와 파이프라인이 필요하다는 것을 알 수 있습니다.

따라서 우리는 프로젝트와 파이프라인을 만들고 이들을 연결해야 합니다. 일반적으로 factories에서 하위 요소는 부모를 인수로 필요로 합니다. 이 경우 파이프라인은 프로젝트의 자식입니다. 따라서 우리는 먼저 프로젝트를 만들고, 그런 다음 파이프라인을 만들 때 프로젝트를 인수로 전달하여 파이프라인을 프로젝트에 “묶을” 수 있습니다. 파이프라인은 또한 사용자에 의해 소유되기 때문에 사용자도 필요합니다. 예를 들어, 다음은 프로젝트와 파이프라인을 만드는 예입니다:

  let(:user) { create(:user) }
  let(:project) { create(:project, :repository) }
  let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id, user: user) }

이와 비슷하게, 부모 파이프라인을 전달하여 작업(build)을 만들 수 있습니다:

  create(:ci_build, pipeline: pipeline, stage_idx: 10, stage: 'publish', name: 'CentOS')

이미 많은 팩토리들이 존재하기 때문에 필요한 것이 이미 있는지 확인해야 합니다.

내비게이션

visit 메서드를 사용하여 페이지로 이동할 수 있습니다. 경로를 인수로 전달하면 됩니다. Rails는 자동으로 도우미 경로를 생성하므로 하드코딩된 문자열 대신 이 경로를 사용해야 합니다. 경로는 route 모델을 사용하여 생성되므로 파이프라인으로 이동하려면 다음을 사용해야 합니다:

  visit project_pipeline_path(project, pipeline)

내비게이션하거나 UI를 통해 비동기적 호출을 수행하기 전에 반드시 wait_for_requests를 사용하여 추가 지시를 진행하기 전에 요청을 기다려야 합니다.

요소 상호 작용

여러 가지 다양한 방법으로 요소를 찾고 상호 작용할 수 있습니다. 최상의 실천법은 UI 테스팅 섹션을 참조하세요.

버튼을 클릭하려면 버튼 안에 있는 텍스트를 사용하여 click_button을 사용하세요:

  click_button '버튼 요소 내의 텍스트'

링크를 따라가려면 click_link를 사용하세요:

  click_link '링크 태그 안의 텍스트'

입력/양식 요소를 입력하려면 fill_in을 사용하세요. 첫 번째 인수는 셀렉터이고, 두 번째는 with:로 값을 전달하는 것입니다.

  fill_in 'current_password', with: '123devops'

또는 이전 텍스트를 지우지 않고 필드에 키를 추가하려면 find 셀렉터와 함께 send_keys를 사용하거나, 입력 요소의 값을 완전히 바꾸려면 set을 사용할 수 있습니다.

자세한 작업 목록은 feature tests actions 문서에서 확인할 수 있습니다.

어설션

페이지에서 어떤 것이든 어설션하려면, 페이지 문서를 자동으로 정의하는 page 변수에 항상 액세스할 수 있습니다. 이는 page가 선택기나 내용과 같은 특정 구성 요소를 가질 수 있다는 것을 의미합니다. 다음은 몇 가지 예시입니다:

  # 버튼 찾기
  expect(page).to have_button('제출 리뷰')
  # 텍스트로 찾기
  expect(page).to have_text('빌드')
  # `href` 값으로 찾기
  expect(page).to have_link(pipeline.ref)
  # 데이터 테스트 ID로 찾기
  # CSS 셀렉터와 같이, 구체적인 매처가 없을 때 허용됩니다.
  expect(page).to have_css('[data-testid="pipeline-multi-actions-dropdown"]')
  # CSS 셀렉터로 찾기. 이는 마지막 수단입니다.
  # 원하는 요소에 속성을 추가할 수 없을 때 예를 들어 사용됩니다.
  expect(page).to have_css('.js-icon-retry')
  # `not_to`로 이러한 선택기 중 하나를 조합할 수 있습니다.
  expect(page).not_to have_button('제출 리뷰')
  # 테스트 케이스가 이어지는 기대치 할 때는,
  # `:aggregate_failures`를 사용하여 그룹화하는 것이 좋습니다.
  it 'shows the issue description and design references', :aggregate_failures do
    expect(page).to have_text('내가 언급한 디자인')
    expect(page).to have_link(design_tab_ref)
    expect(page).to have_link(design_ref_a)
    expect(page).to have_link(design_ref_b)
  end

또한 다음을 참조하는 하위 블록을 만들 수 있습니다:

  • 당신의 어설션을 하는 범위를 좁히고 의도하지 않은 다른 요소를 찾는 위험을 줄입니다.
  • 요소가 올바른 경계 내에서 찾아졌는지 확인합니다.
  page.within('[data-testid="pipeline-multi-actions-dropdown"]') do
    ...
  end

구체적인 매처 목록은 feature tests matchers 문서에서 확인할 수 있습니다.

피처 플래그

기본적으로 모든 피처 플래그는 YAML 정의나 GDK에서 수동으로 설정한 플래그에 관계없이 활성화됩니다. 피처 플래그가 비활성화된 경우를 테스트하려면 이 플래그를 수동으로 스텁해야 합니다. 이상적으로는 before do 블록에서 수행하세요.

  stub_feature_flags(my_feature_flag: false)

ee 피처 플래그를 수동으로 스텁하는 경우:

  stub_licensed_features(my_feature_flag: false)

디버깅

실제 브라우저를 열려면 접두사 WEBDRIVER_HEADLESS=0로 스펙을 실행할 수 있습니다. 그러나 스펙은 빠르게 지나가고 주변을 살펴볼 시간을 주지 않습니다.

이 문제를 피하려면, Capybara가 실행을 중지할 위치에 binding.pry를 작성하세요. 그러면 표준적으로 브라우저 안에 들어가게 됩니다. 특정 요소를 찾지 못한 사유를 이해하기 위해 다음을 할 수 있습니다:

  • 요소를 선택합니다.
  • 콘솔 및 네트워크 탭을 사용합니다.
  • 브라우저 콘솔 내에서 셀렉터를 실행합니다.

Capybara가 실행되는 터미널 내에서 next를 실행할 수도 있습니다. 이렇게 하면 각 상호작용을 한 줄씩 확인하여 어떤 문제를 일으키는 지 알 수 있습니다.

ChromeDriver 업데이트

Selenium 4.6부터는 selenium-webdriver 젬에 포함된 Selenium Manager로 ChromeDriver를 자동으로 관리할 수 있습니다. 따라서 더 이상 chromedriver를 수동으로 동기화할 필요가 없습니다.


테스트 문서로 돌아가기