프론트엔드 테스트 표준 및 스타일 가이드라인

GitLab에서 프론트엔드 코드를 개발할 때 두 가지 유형의 테스트 스위트를 사용합니다.

JavaScript 단위 및 통합 테스트를 위해 Jest를 사용하고,

e2e(End-to-End) 통합 테스트를 위해 RSpec 기능 테스트와 Capybara를 사용합니다.

모든 신규 기능에 대해서는 단위 및 기능 테스트를 작성해야 합니다.

대부분의 경우, 기능 테스트에는 RSpec을 사용하는 것이 좋습니다.

기능 테스트 시작 방법에 대한 자세한 정보는 기능 테스트 시작하기를 참조하세요.

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

GitLab의 일반적인 테스트 관행에 대한 추가 정보는 테스트 표준 및 스타일 가이드라인 페이지를 참조하세요.

Vue.js 테스트

Vue 구성 요소 테스트에 대한 가이드를 찾고 있다면, 이 섹션으로 바로 이동할 수 있습니다.

Vue 3 테스트에 대한 정보는 이 페이지에 포함되어 있습니다.

Jest

프론트엔드 단위 및 통합 테스트를 작성하기 위해 Jest를 사용합니다.

Jest 테스트는 /spec/frontend 및 EE의 /ee/spec/frontend에서 찾을 수 있습니다.

jsdom의 제한 사항

Jest는 테스트를 실행하기 위해 브라우저 대신 jsdom을 사용합니다.

이로 인해 몇 가지 제한 사항이 있습니다. 즉:

브라우저에서 Jest 테스트를 실행하는 지원 문제를 참조하세요.

Jest 테스트 디버깅

yarn jest-debug를 실행하면 디버그 모드에서 Jest가 실행되어, Jest 문서에 설명된 대로 디버그/검사가 가능합니다.

타임아웃 오류

Jest의 기본 타임아웃은 /jest.config.base.js에 설정되어 있습니다.

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

테스트 성능을 개선할 수 없는 경우, 전체 스위트에 대한 타임아웃을 jest.setTimeout 을 사용하여 늘릴 수 있습니다.

jest.setTimeout(500);

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

혹은 it 에 세 번째 인수를 제공하여 특정 테스트에 대해 설정할 수 있습니다.

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

각 테스트의 성능은 환경에 따라 달라진다는 점을 기억하세요.

테스트 전용 스타일시트

RSpec 통합 테스트를 원활하게 하기 위해 두 가지 테스트 전용 스타일시트가 있습니다.
이들은 애니메이션을 비활성화하여 테스트 속도를 개선하거나 Capybara 클릭 이벤트에 대해 타겟팅해야 할 요소를 visible하게 만드는 등 다양한 용도로 사용될 수 있습니다.

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

테스트 환경은 가능하면 프로덕션 환경과 유사해야 하므로, 최소한으로 사용하고 필요할 때만 추가하세요.

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

Jest-specific 워크플로우인 mocks와 spies에 대한 더 세부적인 내용으로 들어가기 전에 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 computed prop을 테스트하는 것은 여기서 당연하게 여겨질 수 있습니다. 그러나 computed 속성이 metricTypes의 길이를 반환하는지를 테스트하는 것은 Vue 라이브러리 자체를 테스트하는 것입니다. 이는 테스트 슈트에 추가될 뿐입니다. 컴포넌트를 사용자와의 상호작용 방식으로 테스트하는 것이 더 낫습니다: 렌더링된 템플릿을 확인하는 것입니다.

// 나쁜 예
describe('computed', () => {
  describe('hasMetricTypes', () => {
    it('metricTypes가 존재하면 true를 반환합니다.', () => {
      factory({ metricTypes });
      expect(wrapper.vm.hasMetricTypes).toBe(2);
    });

    it('metricTypes가 없으면 true를 반환합니다.', () => {
      factory();
      expect(wrapper.vm.hasMetricTypes).toBe(0);
    });
  });
});

// 좋은 예
it('metricTypes가 존재하면 드롭다운을 표시합니다.', () => {
  factory({ metricTypes });
  expect(wrapper.findComponent(GlDropdown).exists()).toBe(true);
});

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

이러한 종류의 테스트에 주의하세요. 이는 로직을 업데이트하는 것을 더 취약하고 지루하게 만듭니다. 이는 다른 라이브러리에도 해당됩니다. 여기서 제안하는 것은 wrapper.vm 속성을 확인하고 있다면, 테스트를 중지하고 대신 렌더링된 템플릿을 확인하기 위해 재검토해야 한다는 것입니다.

더 많은 예제는 프론트엔드 단위 테스트 섹션에서 찾을 수 있습니다.

모킹을 테스트하지 마세요

또 다른 일반적인 함정은 사양이 모킹이 작동하는지를 검증하는 것입니다. 모킹을 사용하는 경우, 모킹은 테스트를 지원해야 하지만 테스트의 목표가 되어서는 안 됩니다.

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

// 나쁜 예
expect(idGenerator.create()).toBe('1234')

// 좋은 예: 컴포넌트의 로직에 실제로 집중하고 제어 가능한 모킹의 출력을 활용하십시오.
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('존재함', () => {
  // 최선 (특히 통합 테스트에 적합)
  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'); // shallowMountExtended 또는 mountExtended와 함께 사용할 때 – 아래 확인

  // 나쁨
  wrapper.find({ ref: 'foo'});
  wrapper.find('.js-foo');
  wrapper.find('.gl-button');
});

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

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

테스트에서 DOM 요소를 쿼리하기 위해 Vue 템플릿 참조를 사용하는 것을 피하십시오. 이는 컴포넌트의 구현 세부 사항이지 공개 API가 아닙니다.

자식 컴포넌트 쿼리하기

@vue/test-utils로 Vue 컴포넌트를 테스트할 때, DOM 노드를 쿼리하는 대신 자식 컴포넌트를 쿼리하는 또 다른 접근 방식이 있습니다. 이는 테스트 대상의 행동의 구현 세부 사항이 해당 컴포넌트의 개별 단위 테스트에서 다루어져야 한다고 가정합니다. 테스트가 테스트 대상 컴포넌트의 예상 행동을 신뢰성 있게 다룬다면 DOM 쿼리 또는 컴포넌트 쿼리를 작성하는 데 강한 선호는 없습니다.

예시:

it('존재함', () => {
  wrapper.findComponent(FooComponent);
});

단위/컴포넌트 테스트 이름 지정

단위/컴포넌트 테스트는 ${componentName}_spec.js 형식으로 이름을 지정해야 합니다.

테스트 이름이 충분히 구체적이지 않다면, 컴포넌트의 이름을 변경하는 것을 고려하십시오.

예를 들어:

diff_stats_dropdown.vuediff_stats_dropdown_spec.js라는 이름의 단위/컴포넌트 테스트를 가져야 합니다.

Describe 블록 이름 지정

특정 함수/메서드를 테스트하기 위해 describe 테스트 블록을 작성할 때, 메서드 이름을 describe 블록 이름으로 사용하십시오.

나쁨:

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

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

좋음:

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

프로미스 테스트하기

프로미스를 테스트할 때는 항상 테스트가 비동기적인지 확인하고 거부가 처리되도록 해야 합니다. 이제 테스트 스위트에서 async/await 구문을 사용하는 것이 가능합니다:

it('프로미스를 테스트함', async () => {
  const users = await fetchUsers()
  expect(users.length).toBe(42)
});

it('프로미스 거부를 테스트함', async () => {
  await expect(user.getUserName(1)).rejects.toThrow('1번 사용자 를 찾을 수 없습니다.');
});

테스트 함수에서 프로미스를 반환할 수도 있습니다.

프로미스를 다룰 때 donedone.fail 콜백을 사용하는 것은 권장되지 않습니다. 이들은 사용되지 않아야 합니다.

나쁨:

// 반환 누락
it('프로미스를 테스트함', () => {
  promise.then(data => {
    expect(data).toBe(asExpected);
  });
});

// done/done.fail 사용
it('프로미스를 테스트함', done => {
  promise
    .then(data => {
      expect(data).toBe(asExpected);
    })
    .then(done)
    .catch(done.fail);
});

좋음:

// 해결된 프로미스 검증
it('프로미스를 테스트함', () => {
  return promise
    .then(data => {
      expect(data).toBe(asExpected);
    });
});

// Jest의 `resolves` 매처를 사용한 해결된 프로미스 검증
it('프로미스를 테스트함', () => {
  return expect(promise).resolves.toBe(asExpected);
});

// Jest의 `rejects` 매처를 사용한 거부된 프로미스 검증
it('프로미스 거부를 테스트함', () => {
  return expect(promise).rejects.toThrow(expectedError);
});

시간 조작하기

때때로 시간에 민감한 코드를 테스트해야 합니다. 예를 들어, 매 X초마다 실행되는 반복 이벤트와 유사한 경우입니다. 이에 대처하기 위한 몇 가지 전략은 다음과 같습니다:

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

애플리케이션 자체가 일부 시간을 기다리고 있는 경우, 기다림을 모의하여 기다려야 합니다. Jest에서는 이 작업이 기본적으로 자동으로 수행됩니다 (또한 Jest 타이머 모의도 참조하십시오).

const doSomethingLater = () => {
  setTimeout(() => {
    // 작업 수행
  }, 4000);
};

Jest에서:

it('작업을 수행함', () => {
  doSomethingLater();
  jest.runAllTimers();

  expect(something).toBe('완료됨');
});

Jest에서 현재 위치를 모킹하기

note
각 테스트 전에 window.location.href의 값이 초기화되어 이전 테스트가 이후 테스트에 영향을 미치는 것을 방지합니다.

테스트에서 window.location.href가 특정 값을 가져야 하는 경우, setWindowLocation 헬퍼를 사용하세요:

import setWindowLocation from 'helpers/set_window_location_helper';

it('passes', () => {
  setWindowLocation('https://gitlab.test/foo?bar=true');

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

해시만 수정하려면 setWindowLocation 헬퍼를 사용하거나 직접 window.location.hash에 할당하면 됩니다. 예를 들어:

it('passes', () => {
  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('passes', () => {
  window.location.reload();

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

테스트에서 기다리기

때때로 테스트는 계속 진행하기 전에 애플리케이션에서 어떤 일이 발생하기를 기다려야 합니다.

다음과 같은 행동은 피해야 합니다:

  • setTimeout 사용은 기다리는 이유를 불분명하게 만듭니다. 또한, 테스트에서 이를 가짜로 처리하기 때문에 사용이 까다롭습니다.
  • setImmediate 사용은 Jest 27 이후로 더 이상 지원되지 않습니다. 자세한 내용은 이 에픽을 참조하세요.

프라미스와 Ajax 호출

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

const askTheServer = () => {
  return axios
    .get('/endpoint')
    .then(response => {
      // 어떤 작업을 수행
    })
    .catch(error => {
      // 다른 작업을 수행
    });
};

Jest에서:

it('waits for an Ajax call', async () => {
  await askTheServer()
  expect(something).toBe('done');
});

Promise에 핸들러를 등록할 수 없는 경우, 예를 들어 동기 Vue 생명주기 후크에서 실행되는 경우, waitFor 헬퍼를 사용하거나 모든 대기 중인 Promise를 플러시합니다:

Jest에서:

it('waits for an Ajax call', async () => {
  synchronousFunction();

  await waitForPromises();

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

Vue 렌더링

Vue 컴포넌트가 다시 렌더링될 때까지 기다리려면 nextTick()을 사용하세요.

Jest에서:

import { nextTick } from 'vue';

// ...

it('renders something', async () => {
  wrapper.setProps({ value: 'new value' });

  await nextTick();

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

이벤트

애플리케이션이 테스트에서 기다려야 하는 이벤트를 트리거할 경우, 어설션을 포함하는 이벤트 핸들러를 등록합니다:

it('waits for an event', () => {
  eventHub.$once('someEvent', eventHandler);

  someFunction();

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

Jest에서는 이를 위해 Promise를 사용할 수도 있습니다:

it('waits for an event', () => {
  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()를 수동으로 호출할 필요가 없습니다.

하지만, 일부 모의(mock) 객체, 스파이(spy), 및 픽스쳐(fixture)는 제거되어야 하며, 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)
  }
}

이런 호출에 대한 테스트 케이스를 작성하면서 올바른 매개변수로 호출되는지 확인하기 위해 리졸버를 사용할 수 있습니다.

예를 들어, 래퍼를 생성할 때 리졸버가 쿼리나 변이에 매핑되도록 해야 합니다.

여기서 모의(mock)하고 있는 변이는 setActiveBoardItem입니다:

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

다음 코드에서는 네 개의 인수를 전달해야 합니다. 두 번째 인수는 모의(mock)된 쿼리나 변이의 입력 변수 컬렉션이어야 합니다.

변이가 올바른 매개변수로 호출되는지 테스트하기 위해:

it('close에서 setActiveBoardItemMutation을 호출한다', async () => {
    wrapper.findComponent(GlDrawer).vm.$emit('close');

    await waitForPromises();

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

Jest 모범 사례

원시 값 비교 시 toBetoEqual보다 선호하기

Jest에는 toBetoEqual 매처가 있습니다.

toBeObject.is를 사용하여 값을 비교하므로 기본적으로 toEqual보다 빠릅니다.

후자는 결국 Object.is를 사용하여 값을 비교하지만, 원시 값의 경우 복잡한 객체를 비교해야 할 때만 사용해야 합니다.

예시:

const foo = 1;

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

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

더 적합한 매처 사용 선호하기

Jest는 테스트를 더 읽기 쉽고 이해하기 쉬운 오류 메시지를 생성하도록 만들어주는 유용한 매처인 toHaveLength 또는 toBeUndefined를 제공합니다. 전체 매처 목록은 문서를 확인하세요.

예시:

const arr = [1, 2];

// 출력:
// 예상 길이: 1
// 수신 길이: 2
expect(arr).toHaveLength(1);

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

// 출력:
// expect(received).toBe(expected) // Object.is 동등성
// 예상: undefined
// 수신: "bar"
const foo = 'bar';
expect(foo).toBe(undefined);

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

toBeTruthy 또는 toBeFalsy 사용 피하기

Jest는 또한 toBeTruthytoBeFalsy라는 매처를 제공합니다. 이러한 매처는 테스트를 약하게 만들고 잘못된 긍정적 결과를 생성하기 때문에 사용하지 않아야 합니다.

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

까다로운 toBeDefined 매처

Jest는 잘못된 긍정적인 테스트를 생성할 수 있는 까다로운 toBeDefined 매처를 가지고 있습니다. 왜냐하면 주어진 값을 검증하여 undefined만 처리하기 때문입니다.

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

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

setImmediate 사용 피하기

setImmediate 사용을 피하십시오. setImmediate는 I/O가 완료된 후에 콜백을 실행하기 위한 임시 솔루션입니다. 이는 웹 API의 일부가 아니므로, 단위 테스트에서 NodeJS 환경을 대상으로 합니다.

setImmediate 대신에 jest.runAllTimers 또는 jest.runOnlyPendingTimers를 사용하여 대기 중인 타이머를 실행하십시오. 후자는 코드에 setInterval이 있을 때 유용합니다. 기억하세요: 우리의 Jest 구성은 가짜 타이머를 사용합니다.

비결정적 스펙 피하기

비결정성은 잘못된 스펙과 덜컹거리는 스펙의 온상입니다. 이러한 스펙은 CI 파이프라인을 깨뜨리고 다른 기여자들의 작업 흐름을 방해합니다.

  1. 테스트 대상의 협력자(예: Axios, Apollo, Lodash 헬퍼) 및 테스트 환경(예: Date)이 시스템 간 및 시간 경과에 따라 일관되게 작동하는지 확인하십시오.
  2. 테스트가 집중되어 있고 개별 테스트에서 “추가 작업”을 수행하지 않도록 하십시오(예: 불필요하게 테스트 대상을 한 번 이상 생성하지 않도록).

결정성을 위한 Date 가짜 만들기

Date는 기본적으로 우리의 Jest 환경에서 가짜로 설정됩니다. 이는 Date() 또는 Date.now()를 호출할 때마다 고정된 결정적 값을 반환함을 의미합니다.

기본 가짜 날짜를 변경해야 하는 경우, describe 블록 내에서 useFakeDate를 호출하여 해당 describe 컨텍스트 내에서만 날짜가 바뀝니다:

import { useFakeDate } from 'helpers/fake_date';

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

  // NOTE: `useFakeDate`는 테스트 실행 중에 호출할 수 없습니다 (즉, `it`, `beforeEach`, `beforeAll` 등 안에).
  describe("Ada Lovelace의 생일에", () => {
    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';

// NOTE: `useRealDate`는 테스트 실행 중에 호출할 수 없습니다 (즉, `it`, `beforeEach`, `beforeAll` 등 안에).
describe('실제 날짜로', () => {
  useRealDate();
});

결정론을 위한 Math.random 속이기

테스트 주제가 Math.random에 의존하는 경우 이를 속임수로 대체하는 것을 고려하십시오.

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

테스트에서의 콘솔 경고 및 오류

예기치 않은 콘솔 경고 및 오류는 우리의 프로덕션 코드에서 문제를 나타냅니다.

우리는 테스트 환경을 엄격하게 유지하고 싶으므로, 예기치 않은

console.error 또는 console.warn 호출이 발생할 경우 테스트는 실패해야 합니다.

감시자의 콘솔 메시지 무시하기

우리의 제어 외부에 많은 코드가 존재하므로, 기본적으로 무시되는 일부 콘솔 메시지가 있습니다.

이 메시지들은 사용될 경우 테스트를 실패하지 않습니다. 무시되는 메시지 목록은

setupConsoleWatcher를 호출하는 곳에서 관리할 수 있습니다. 예:

setupConsoleWatcher({
  ignores: [
    ...,
    // `console.error('Foo bar')` 또는 `console.warn('Foo bar')` 호출은 우리의 콘솔 감시자에 의해 무시됩니다.
    'Foo bar',
    // 유연한 메시지 매칭을 허용하기 위한 정규 표현식 사용.
    /Lorem ipsum/,
  ]
});

특정 테스트가 describe 블록에 대해 특정 메시지를 무시해야 하는 경우,

describe의 상단 근처에 ignoreConsoleMessages 도우미를 사용합니다.

이것은 테스트 컨텍스트에 대해 무시된 메시지를 설정/해제하기 위해 beforeAllafterAll을 자동으로 호출합니다.

이것은 절대적으로 테스트 유지 관리가 필요할 때에만 신중하게 사용하십시오. 예:

import { ignoreConsoleMessages } from 'helpers/console_watcher';

describe('foos/components/foo.vue', () => {
  describe('blooped 시', () => {
    // `console.warn('Lorem ipsum')` 호출 시 테스트가 실패하지 않습니다.
    ignoreConsoleMessages([
      /^Lorem ipsum/
    ]);
  });

  describe('기본값', () => {
    // `console.warn('Lorem ipsum')` 호출 시 테스트가 실패합니다.
  });
});

팩토리

TBU

Jest와 함께하는 Mocking 전략

Stubbing 및 Mocking

Stubs 또는 spies는 종종 동의어로 사용됩니다. Jest에서는 .spyOn 메서드 덕분에 매우 쉽습니다.

공식 문서

더 어려운 부분은 함수나 심지어 종속성에 사용할 수 있는 mocks입니다.

수동 모듈 mocks

수동 mocks는 전체 Jest 환경에서 모듈을 모방하는 데 사용됩니다.

이것은 테스트 환경에서 쉽게 소비될 수 없는 모듈을 모방하여 단위 테스트를 단순화하는 매우 강력한 도구입니다.

경고: 모든 스펙에서 일관되게 적용되지 않아야 하는 mock을 사용해서는 안 됩니다 (즉, 몇몇 스펙에서만 필요함). 대신, 관련 스펙 파일에서 jest.mock(..) (또는 유사한 mocking 함수)를 사용하는 것을 고려하십시오.

수동 mocks는 어디에 두어야 합니까?

Jest는 모듈 옆에 __mocks__/ 디렉터리에 모의(mock)를 배치하여 수동 모듈 mocks를 지원합니다.

(예: app/assets/javascripts/ide/__mocks__). 이렇게 하지 마십시오. 모든 테스트 관련 코드를 단일 위치(즉, spec/ 폴더)에 유지하고 싶습니다.

node_modules 패키지에 대한 수동 mock이 필요한 경우, spec/frontend/__mocks__ 폴더를 사용하세요. 여기

monaco-editor 패키지에 대한 Jest mock의 예가 있습니다.

CE 모듈에 대한 수동 mock이 필요한 경우, 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 형식을 사용하지만 jest 환경과 호환되지 않기 때문에 유용합니다. 이 모의는 어떤 동작도 제거하지 않고, 단지 멋진 es6 호환 래퍼를 제공합니다.

  • __mocks__/monaco-editor/index.js - 이 모의는 Monaco 패키지가 Jest 환경에서 완벽히 호환되지 않기 때문에 유용합니다. 사실, webpack은 작동을 위해 특별한 로더를 요구합니다. 이 모의는 Jest에서 이 패키지를 사용할 수 있게 합니다.

모의를 가볍게 유지하기

전역 모의는 마법을 도입하고 기술적으로 테스트 범위를 줄일 수 있습니다. 모의가 유익하다고 판단되면:

  • 모의를 짧고 집중적으로 유지하십시오.

  • 왜 이 모의가 필요한지에 대한 상위 주석을 모의에 남깁니다.

추가 모의 기술

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

프론트엔드 테스트 실행

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

프론트엔드 테스트를 실행하려면 다음 명령어가 필요합니다:

  • rake frontend:fixturesfixtures를 (재)생성합니다. 이들을 요구하는 테스트를 실행하기 전에 fixture가 최신 상태인지 확인하세요.

  • yarn jest는 Jest 테스트를 실행합니다.

라이브 테스트 및 집중 테스트 – Jest

테스트 스위트를 작업하는 동안 이러한 사양을 감시 모드에서 실행하여 각 저장 시 자동으로 다시 실행되게 하고 싶을 수도 있습니다.

# 이름 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/
# 경로에 term이 포함된 모든 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();

  // ...
});

픽스쳐 생성

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

  • spec/frontend/fixtures/, CE에서 테스트를 실행할 때.
  • ee/spec/frontend/fixtures/, EE에서 테스트를 실행할 때.

픽스쳐를 생성하려면 다음을 실행하십시오:

  • 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

# 현재 체크아웃된 브랜치 대신 로컬 마스터 브랜치의 커밋을 살펴봅니다.
$ 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로 표시되면 자동으로 설정됩니다.

새로운 픽스쳐를 만들 때는 (ee/)spec/controllers/ 또는 (ee/)spec/requests/에서 해당 엔드포인트에 대한 테스트를 살펴보는 것이 종종 유용합니다.

GraphQL 쿼리 픽스쳐

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

# 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.

JSON 픽스쳐는 test_fixtures 별칭을 사용하여 Jest 테스트에서 가져올 수 있습니다. 이전에 기술된 대로.

데이터 기반 테스트

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'} | ${'파이프라인 실패 - 아쉬움'}
    ${true}  | ${'pipeline-passed'} | ${'파이프라인 성공 - 승리!'}
`('파이프라인 컴포넌트', ({ status, icon, message }) => {
    it(`상태가 ${status}일 때 아이콘 ${icon}을 반환합니다.`, () => {
        expect(icon(status)).toEqual(message)
    })

    it(`상태가 ${status}일 때 메시지 ${message}를 반환합니다.`, () => {
        expect(message(status)).toEqual(message)
    })
});

주의 사항

JavaScript로 인한 RSpec 오류

기본적으로 RSpec 유닛 테스트는 헤드리스 브라우저에서 JavaScript를 실행하지 않으며, rails에서 생성된 HTML을 검사하는 데 의존합니다.

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

RSpec 테스트에서 JavaScript 드라이버를 활성화하려면, JavaScript가 필요할 때 개별 스펙이나 여러 스펙이 포함된 컨텍스트 블록에 :js를 추가합니다:

# 한 개의 스펙에 대해
it '학대 신고에 대한 정보를 제시합니다', :js do
  # assertions...
end

describe "Admin::AbuseReports", :js do
  it '학대 신고에 대한 정보를 제시합니다' do
    # assertions...
  end
  it '학대 신고에 추가 버튼을 보여줍니다' do
    # assertions...
  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가 타임아웃될 수 있습니다. 이렇게 즉시 가져와서 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 Helper: 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 컴포넌트가 생명주기의 일부로 요청을 할 때와 같이 요청의 Promise 핸들이 없는 경우에 매우 유용합니다.

  • waitFor(url, callback): 요청이 끝난 후에 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('finds elements with `findByTestId`', () => {
    expect(wrapper.findByTestId('gitlab-frontend-stack').exists()).toBe(true);
  });

  it('finds elements with `findByText`', () => {
    expect(wrapper.findByText('GitLab 프론트엔드 스택').exists()).toBe(true);
    expect(wrapper.findByText('TypeScript').exists()).toBe(false);
  });

  it('finds elements with `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에 로그인하세요.

공유 1Password 계정.

Firefox

macOS

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

  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는 스냅샷을 생성할 때 유의해야 할 모범 사례에 대한 훌륭한 문서를 제공합니다.

스냅샷은 어떻게 작동하나요?

스냅샷은 함수 호출의 왼쪽에서 테스트할 것을 요청하는 것의 문자열화된 버전입니다. 이는 문자열 형식에 대한 모든 종류의 변경이 결과에 영향을 미친다는 것을 의미합니다. 이 과정은 자동 변환 단계를 위한 직렬 변환기를 활용하여 수행됩니다. Vue의 경우, 적절한 직렬 변환기를 제공하는 vue-jest 패키지를 활용하여 이미 처리되어 있습니다.

사양의 결과가 생성된 스냅샷 파일에 있는 결과와 다르면, 테스트 스위트에서 실패하는 테스트로 이를 알게 됩니다.

자세한 정보는 Jest의 공식 문서에서 확인하세요: https://jestjs.io/docs/snapshot-testing

장단점

장점

  • 중요한 HTML 구조의 우발적인 변경에 대한 좋은 경고를 제공합니다.
  • 설정이 간편합니다.

단점

  • 요소를 찾아 직접 존재 여부를 주장하는 vue-tests-utils가 제공하는 명확성이나 가드 레일이 부족합니다.
  • 컴포넌트를 의도적으로 업데이트할 때 불필요한 소음을 생성합니다.
  • 버그의 스냅샷을 찍을 높은 위험이 있으며, 이는 테스트가 이제 문제를 수정할 때 실패하게 만듭니다.
  • 스냅샷 내에 의미 있는 주장이나 기대가 없어서 이해하거나 대체하기가 더 어렵습니다.
  • GitLab UI와 같은 종속성과 함께 사용 시, 우리가 테스트하고 있는 컴포넌트의 HTML이 변경될 때 테스트에서의 취약성을 초래합니다.

사용할 때

스냅샷을 사용할 때

  • 중요한 HTML 구조를 보호하여 우발적으로 변경되지 않도록 할 때
  • 복잡한 유틸리티 함수의 JS 객체 또는 JSON 출력 주장할 때

사용하지 않을 때

스냅샷을 사용하지 마세요

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

예시

보다시피, 전반적으로 스냅샷 테스트의 단점이 장점보다 훨씬 더 크다. 이를 좀 더 잘 설명하기 위해, 이 섹션에서는 스냅샷 테스트를 사용하고 싶어할 때의 몇 가지 예제를 보여줄 것이며, 왜 이러한 패턴이 좋지 않은지를 설명할 것이다.

예시 #1 - 요소 가시성

요소 가시성을 테스트할 때, 주어진 컴포넌트를 찾기 위해 vue-tests-utils (VTU)를 사용하는 것을 선호하고, 그 다음에 기본 .exists() 메서드 호출을 VTU 래퍼에 사용하세요. 이는 더 나은 가독성과 더 탄력적인 테스트를 제공합니다. 아래 예제를 보면, 스냅샷에 대한 주장이 우리가 기대하는 것을 알려주지 않는다는 것을 알 수 있습니다. 우리는 오직 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을 전달하고 잘못된 가시성을 가질 경우를 상상해보세요: 스냅샷 테스트는 여전히 통과할 것입니다. 문제 있는 HTML을 포함하여 캡처할 것이므로, 스냅샷의 출력을 두 번 확인하지 않는 한 테스트가 깨졌다는 것을 절대 알지 못할 것입니다.

예시 #2 - 텍스트 존재 여부

컴포넌트 내에서 텍스트를 찾는 것은 vue-test-utils 방법인 wrapper.text()를 사용하면 매우 쉽습니다. 그러나 포맷팅이나 HTML 중첩으로 인해 반환된 값에 다소 일관되지 않은 공백이 있을 경우, 스냅샷을 사용하는 것이 유혹적일 수 있는 경우가 있습니다.

이러한 경우에는 공백을 무시하기 위해 스냅샷을 사용하는 것보다 각 문자열을 개별적으로 주장하고 여러 주장을 하는 것이 더 바람직합니다. DOM 레이아웃에 대한 변경이 있더라도 텍스트가 여전히 완벽하게 포맷된 경우에도 스냅샷 테스트가 실패할 것입니다.

<template>
  <gl-sprintf :message="my-message">
    <template #code="{ content }">
      <code>{{ content }}</code>
    </template>
  </gl-sprintf>
  <p> 내 두 번째 메시지 </p>
</template>

나쁜 예:

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

좋은 예:

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

it('단락 텍스트를 렌더링합니다', () => {
  expect(findOtherText().text()).toBe("내 두 번째 메시지")
})

예시 #3 - 복잡한 HTML

매우 복잡한 HTML이 있을 때, 우리는 전체를 포착하기보다는 특정 민감하고 의미 있는 포인트를 확인하는 데 집중해야 합니다. 스냅샷 테스트에서의 가치는 개발자에게 경고하는 것입니다. 즉, 그들이 의도하지 않은 HTML 구조를 실수로 변경했을 수 있다는 점입니다. 변경의 출력이 읽기 어렵다면, 복잡한 HTML 출력의 경우 종종 그런 일이 발생하므로, 무언가가 변경되었다는 신호 자체가 충분한가요? 그리고 그렇다면, 스냅샷 없이도 이를 달성할 수 있을까요?

복잡한 HTML 출력의 좋은 예시로는 GlTable이 있습니다. 스냅샷 테스트는 행과 열 구조를 캡처할 수 있기 때문에 좋은 옵션처럼 보일 수 있지만, 대신 우리가 예상하는 텍스트를 확인하거나 행과 열의 수를 수동으로 세야 합니다.

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

나쁜 예:

it('renders GlTable as I expect', () => {
  expect(findGlTable().element).toMatchSnapshot()
})

좋은 예:

it('renders the right number of rows', () => {
  expect(findGlTable().findAllRows()).toHaveLength(expectedLength)
})

it('renders the special icon that only appears on a full moon', () => {
  expect(findGlTable().findMoonIcon().exists()).toBe(true)
})

it('renders the correct email format', () => {
  expect(findGlTable().text()).toContain('my_strange_email@shaddyprovide.com')
})

비록 더 장황하지만, 이제 우리의 테스트는 GlTable이 내부 구현을 변경하더라도 깨지지 않게 됩니다. 이것은 우리가 리팩토링하거나 테이블에 추가할 때 무엇을 보존해야 하는지를 다른 개발자(혹은 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에 대한 사전 지식이 없음을 가정하고, 이러한 테스트를 언제 어떻게 사용하는지에 대한 명확한 지침을 제공합니다.

기능 테스트를 사용해야 할 때

기능 테스트는 다음과 같은 경우에 사용해야합니다:

  • 여러 구성 요소에 걸쳐 있습니다.
  • 사용자가 여러 페이지를 탐색해야 합니다.
  • 양식을 제출하고 다른 곳에서 결과를 관찰해야 합니다.
  • 단위 테스트로 수행할 경우 엄청난 양의 모의(mocking) 및 스텁(stubbing)이 필요합니다.

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

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

기능 테스트를 사용하지 말아야 할 때

같은 테스트 결과를 얻을 수 있는 경우에는 기능 테스트 대신 jestvue-test-utils 단위 테스트를 사용해야 합니다. 기능 테스트는 실행 비용이 상당히 비쌉니다.

다음과 같은 경우에는 단위 테스트를 사용해야 합니다:

  • 구현하려는 동작이 하나의 구성 요소에 모두 포함되어 있는 경우.
  • 원하는 효과를 유도하기 위해 다른 구성 요소의 동작을 시뮬레이션 할 수 있는 경우.
  • 이미 가상 DOM에서 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는 props를 ChildComponent2로 전달합니다.

대신 단위 테스트를 사용하여 다음을 수행할 수 있습니다:

  • ParentComponent 단위 테스트 파일 내에서 childComponent1에서 예상 이벤트를 발생시키기.
  • prop가 childComponent2로 전달되는지 확인하기.

그런 다음 각 자식 구성 요소는 이벤트가 발생했을 때와 prop이 변경되었을 때 어떤 일이 발생하는지 단위 테스트합니다.

이 예제는 더 큰 규모와 깊은 구성 요소 트리에도 적용됩니다. 자신 있게 자식 구성 요소를 마운트하고, 이벤트를 발생시키거나 가상 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를 접두어로 붙이면 실제 브라우저가 열리고 이를 통해 테스트가 실행되어 디버깅에 매우 유용합니다.

Firefox를 사용하려면, Chrome 대신 다음과 같이 명령어에 WEBDRIVER=firefox를 접두어로 붙입니다.

테스트 작성 방법

기본 파일 구조

  1. 모든 문자열 리터럴을 변경할 수 없게 만드세요.

    모든 기능 테스트의 첫 번째 줄은 다음과 같아야 합니다:

    # frozen_string_literal: true
    

    이는 모든 Ruby 파일에 있으며, 모든 문자열 리터럴을 변경할 수 없게 만듭니다. 성능 향상 효과도 있지만, 이는 이 섹션의 범위를 벗어납니다.

  2. 종속성을 가져옵니다.

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

    require 'spec_helper'
    

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

  3. 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/를 살펴보면, 프로젝트와 파이프라인이 필요하다는 것을 알 수 있습니다.

따라서 프로젝트와 파이프라인을 생성하고 서로 연결해야 합니다. 일반적으로 팩토리에서 자식 요소는 부모를 인수로 받아야 합니다. 이 경우, 파이프라인은 프로젝트의 자식입니다. 따라서 먼저 프로젝트를 생성하고, 파이프라인을 생성할 때 프로젝트를 인수로 전달하여 파이프라인이 프로젝트에 “바인드”되게 할 수 있습니다. 파이프라인은 사용자에게도 소속되므로 사용자도 필요합니다. 예를 들어, 다음은 프로젝트와 파이프라인을 생성하는 코드입니다:

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

같은 맥락에서, 빌드 팩토리를 사용하여 파이프라인 부모를 전달함으로써 작업(빌드)을 생성할 수 있습니다:

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

이미 존재하는 다양한 팩토리가 있으므로, 필요한 것이 있는지 기존 파일을 확인해보는 것이 좋습니다.

탐색

visit 메서드를 사용하고 경로를 인수로 전달하여 페이지로 이동할 수 있습니다.

Rails는 자동으로 헬퍼 경로를 생성하므로 하드코딩된 문자열 대신 이들을 사용해야 합니다.

이것들은 라우트 모델을 사용하여 생성되므로, 파이프라인으로 가려면 다음과 같이 사용합니다:

  visit project_pipeline_path(project, pipeline)

UI를 통해 탐색하거나 비동기 호출을 할 때 페이지 상호작용을 실행하기 전에 wait_for_requests를 사용하여 추가 지침을 진행하기 전에 준비하십시오.

요소 상호작용

요소를 찾고 상호작용하는 다양한 방법이 있습니다.

모범 사례를 보려면 UI 테스트 섹션을 참조하십시오.

버튼을 클릭하려면 버튼에 있는 문자열로 click_button을 사용하세요:

  click_button 'Text inside the button element'

링크를 따르려면 click_link를 사용합니다:

  click_link 'Text inside the link tag'

fill_in을 사용하여 입력/양식 요소를 채울 수 있습니다. 첫 번째 인수는 선택자, 두 번째는 with:로, 전달할 값입니다.

  fill_in 'current_password', with: '123devops'

또는 find 선택자를 사용하여 send_keys와 쌍을 이루어 이전 텍스트를 제거하지 않고 필드에 키를 추가하거나 set을 사용하여 입력 요소의 값을 완전히 교체할 수 있습니다.

보다 포괄적인 작업 목록은 기능 테스트 작업 문서에서 확인할 수 있습니다.

단언

페이지에서 어떤 것을 단언하기 위해, 자동으로 정의되는 page 변수를 사용할 수 있으며 이는 페이지 문서를 의미합니다. 즉, page가 특정 구성 요소(선택자 또는 콘텐츠)를 가지고 있기를 기대할 수 있습니다. 몇 가지 예시는 다음과 같습니다:

  # 버튼 찾기
  expect(page).to have_button('Submit review')
  # 텍스트로 찾기
  expect(page).to have_text('build')
  # `href` 값으로 찾기
  expect(page).to have_link(pipeline.ref)
  # data-testid로 찾기
  # 특정 매처가 없을 때 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('Submit review')
  # 테스트 케이스가 연속적인 기대를 갖는 경우,
  # `:aggregate_failures`를 사용하여 그룹화하는 것이 좋습니다.
  it '문제 설명과 디자인 참조를 표시합니다', :aggregate_failures do
    expect(page).to have_text('The designs I mentioned')
    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

보다 포괄적인 매처 목록은 기능 테스트 매처 문서에서 확인할 수 있습니다.

기능 플래그

기본적으로 모든 기능 플래그는 YAML 정의나 GDK에서 수동으로 설정한 플래그에 관계없이 활성화되어 있습니다. 기능 플래그가 비활성화된 상태를 테스트하려면, 수동으로 플래그를 스텁해야 하며, 이상적으로는 before do 블록에서 진행해야 합니다.

  stub_feature_flags(my_feature_flag: false)

ee 기능 플래그를 스텁하고자 할 경우, 다음과 같이 사용하십시오:

  stub_licensed_features(my_feature_flag: false)

브라우저 콘솔 오류 확인

기본적으로 기능 사양은 브라우저 콘솔 오류가 발견되더라도 실패하지 않습니다. 때때로 예상치 못한 콘솔 오류가 없음을 확인하고 싶습니다. 이는 통합 문제를 나타낼 수 있습니다.

브라우저 콘솔 오류가 발생할 경우 기능 사양이 실패하도록 설정하려면, BrowserConsoleHelpers 지원 모듈의 expect_page_to_have_no_console_errors를 사용하십시오:

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

  # ...
end

참고: expect_page_to_have_no_console_errorsWEBDRIVER=firefox에서 작동하지 않습니다. 로그는 Chrome 드라이버를 사용할 때만 캡처됩니다.

때때로 무시하고 싶은 알려진 콘솔 오류가 있습니다. 메시지가 관찰되어도 테스트가 실패하지 않도록 하기 위해, expect_page_to_have_no_console_errorsallow: 매개변수를 전달할 수 있습니다:

RSpec.describe 'Pipeline', :js do
  after do
    expect_page_to_have_no_console_errors(allow: [
      "Blow up!",
      /Foo.*happens/
    ])
  end

  # ...
end

spec/support/helpers/browser_console_helpers.rb에서 BROWSER_CONSOLE_ERROR_FILTER 상수를 업데이트하여 전역적으로 무시해야 하는 콘솔 오류 목록을 변경할 수 있습니다.

디버깅

WEBDRIVER_HEADLESS=0 접두사를 사용하여 실제 브라우저를 열어 스펙을 실행할 수 있습니다. 그러나 스펙이 명령을 빠르게 진행하기 때문에 둘러볼 시간이 없게 됩니다.

이 문제를 피하려면, Capybara가 실행 중지를 원하는 라인에 binding.pry를 작성하십시오. 그러면 표준 사용 방식으로 브라우저 안에 있게 됩니다. 특정 요소를 찾을 수 없는 이유를 이해하려면 다음을 수행할 수 있습니다:

  • 요소 선택.
  • 콘솔 및 네트워크 탭 사용.
  • 브라우저 콘솔 내에서 선택기 실행.

Capybara가 실행 중인 터미널 내에서 next를 실행할 수도 있으며, 이는 테스트를 한 줄씩 진행합니다. 이 방식으로 각 상호작용을 하나씩 확인하여 문제가 발생할 수 있는 원인을 파악할 수 있습니다.

GDK에서 실행 시간 개선하기

Jest 테스트 스위트를 실행할 때, 사용 가능한 코어의 60%를 사용하는 작업자 수가 설정됩니다. 이는 더 빠른 실행 시간을 가져오지만 메모리 사용량이 증가합니다. 이와 관련하여 더 많은 벤치마크는 이 문제를 참조하십시오.

ChromeDriver 업데이트

Selenium 4.6부터 ChromeDriver는 selenium-webdriver gem과 함께 제공되는 Selenium Manager에 의해 자동으로 관리될 수 있습니다. 더 이상 chromedriver를 수동으로 동기화할 필요가 없습니다.


테스트 문서로 돌아가기