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

프런트엔드 코드를 개발하는 동안 만나는 테스트 스위트에는 두 가지 유형이 있습니다. 우리는 JavaScript 유닛 및 통합 테스트에 Jest를 사용하고, e2e (end-to-end) 통합 테스트에는 RSpec feature 테스트와 Capybara를 사용합니다.

새로운 기능에 대해 유닛 및 피처 테스트를 작성해야 합니다. 대부분의 경우, 피처 테스트에는 RSpec를 사용해야 합니다. 피처 테스트를 시작하는 방법에 대한 자세한 내용은 피처 테스트 시작하기를 참조하세요.

버그 수정을 위해 회귀 테스트를 작성하여 미래에 재발되지 않도록 합니다.

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

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을 사용하여 전체 스위트의 타임아웃을 늘릴 수 있습니다 (https://jestjs.io/docs/next/jest-object#jestsettimeouttimeout).

jest.setTimeout(500);

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

또는 it에 세 번째 인수를 제공하여 특정 테스트의 타임아웃을 늘릴 수도 있습니다.

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

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

테스트별 스타일시트

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

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

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

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

모의 객체의 내부를 테스트하지 마세요.

라이브러리는 JavaScript 개발자의 삶에서 중요한 부분입니다. 일반적으로 라이브러리 내부를 테스트하지 않고, 라이브러리가 의도한 대로 작동하고 자체적으로 테스트 커버리지가 있다고 기대해야 합니다.

Vue 영역을 간단히 살펴봅시다. Vue는 GitLab JavaScript 코드베이스의 중요한 부분입니다. Vue 컴포넌트에 대한 스펙을 작성할 때 흔한 함정은 Vue에서 제공하는 기능을 실제로 테스트하게 되는 것입니다. 다음은 코드베이스에서 가져온 예시입니다.

반면, 계산된 속성 hasMetricTypes의 테스트는 metricTypes의 길이를 반환하는지 테스트하는 것은 Vue 라이브러리 자체를 테스트하는 것입니다. 이는 테스트 스위트를 추가하는 것 외에는 가치가 없습니다. 사용자 상호작용과 관련하여 컴포넌트를 테스트하는 것이 더 나은 방법입니다: 렌더링된 템플릿을 확인하는 것입니다.

이와 같은 테스트에 주의하세요. 이는 업데이트 로직을 더 혹사하고 성가신 작업으로 만들지 않도록 주의해야 합니다. 이는 다른 라이브러리에도 해당됩니다. 여기서 제안하는 것은: wrapper.vm 속성을 확인하는 경우, 렌더링된 템플릿을 확인하는 대신 테스트를 재고하고 다시 생각하는 것입니다.

추가 예시는 프런트엔드 유닛 테스트 섹션에서 찾을 수 있습니다.

모의 객체를 테스트하지 마세요.

일반적으로 모의 객체가 작동하는 것을 확인하는 대신 사용하기 위해 모의 객체를 확인하는 것이 일반적인 함정입니다.

사용 중이라면 보호되어야 하지만 테스트의 대상이 되어서는 안됩니다.

유저의 흔적을 따르세요.

컴포넌트 중심의 세계에서 유닛 테스트와 통합 테스트의 경계가 모호할 수 있습니다. 가장 중요한 지침은 다음과 같습니다:

  • 미래에 망가질 수 있는 복잡한 로직을 격리하여 테스트할 가치가 있는 경우 깨끗한 유닛 테스트를 작성하세요
  • 그렇지 않으면 테스트지점을 사용자의 흐름에 가깝게 작성하세요

예를 들어, 생성된 마크업을 사용하여 버튼 클릭을 유도하고 마크업이 그에 따라 변화했는지 확인하는 것이 매뉴얼으로 메소드를 호출하고 데이터 구조 또는 계산된 속성을 확인하는 것보다 나은 방법입니다. 테스트가 통과되고 안전성의 오해를 제공하지 않도록 사용자 흐름을 실수로 망가뜨릴 가능성이 항상 있습니다.

공통 관행

이 가이드에 포함된 몇 가지 일반적인 일반 관행은 우리의 테스트 스위트의 일부로서 포함됩니다. 이 가이드에 따르지 않는 것을 발견한다면 이상적으로 즉시 수정하십시오. 🎉

DOM 요소 쿼리 방법

테스트에서 DOM 요소를 쿼리하는 경우, 요소를 고유하게 그리고 의미론적으로 대상으로 하는 것이 가장 좋습니다.

선호적으로는 DOM Testing Library를 사용하여 사용자가 실제로 볼 수 있는 것을 대상으로 하는 것이 좋습니다. 텍스트로 선택할 때는 byRole 쿼리를 사용하는 것이 가장 좋습니다. 이는 접근성에 대한 최선의 관행을 강화하는 데 도움이 됩니다. shallowMountExtended 또는 mountExtended를 사용할 때 옵션으로 DOM Testing Library 쿼리가 사용 가능합니다.

Vue 컴포넌트 단위 테스트를 작성할 때는 컴포넌트별로 자식을 쿼리하는 것이 좋습니다. 이렇게 하면 단위 테스트가 상세한 값 커버리지에 초점을 맞출 수 있기 때문입니다. 그리고 자식 컴포넌트의 동작 복잡성과 대응하기보다는 체계적인 가치를 중시하는 것입니다.

가끔은 위의 어느 것도 실현 가능하지 않을 수 있습니다. 이러한 경우에는 선택자를 간소화하기 위해 테스트 속성을 추가하는 것이 가장 좋은 옵션일 수 있습니다. 가능한 선택자 디렉터리은 다음과 같습니다:

import { shallowMountExtended } from 'helpers/vue_test_utils_helper'

const wrapper = shallowMountExtended(ExampleComponent);

it('exists', () => {
  // Best (especially for integration tests)
  wrapper.findByRole('link', { name: /Click Me/i })
  wrapper.findByRole('link', { name: 'Click Me' })
  wrapper.findByText('Click Me')
  wrapper.findByText(/Click Me/i)
  
  // Good (especially for unit tests)
  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
  
  // Bad
  wrapper.find({ ref: 'foo'});
  wrapper.find('.js-foo');
  wrapper.find('.btn-primary');
});

data-testid 속성의 경우 kebab-case를 사용해야 합니다.

테스트 목적으로만 .js-* 클래스를 추가하는 것은 권장되지 않습니다. 이렇게 할 수 있는 대안이 없는 경우에만 수행하십시오. 테스트에서 Vue 템플릿 ref를 사용하여 DOM 요소를 쿼리하는 것은 권장하지 않습니다. 왜냐하면 그것들은 컴포넌트의 구현 세부사항이 아니라 공개 API가 아니기 때문입니다.

자식 컴포넌트 쿼리

@vue/test-utils를 사용하여 Vue 컴포넌트를 테스트할 때 다른 가능한 접근 방법은 DOM 노드를 쿼리하는 대신 자식 컴포넌트를 쿼리하는 것입니다. 이는 테스트하는 동작의 구현 세부사항이 해당 컴포넌트의 개별 단위 테스트에 의해 커버되어야 한다는 가정에 따릅니다. DOM 또는 컴포넌트 쿼리를 작성할 때 특별한 선호사항은 없습니다. 주의해야 할 것은 테스트가 신뢰할 수 있는 방법으로 테스트하도록 컴포넌트의 예상 동작을 신뢰할 수 있도록 커버하는 것입니다.

예:

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

단위 테스트의 명명

특정 함수/메서드를 테스트하는 설명 테스트 블록을 작성할 때는 메서드 이름을 설명 블록 이름으로 사용해야 합니다.

Bad:

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

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

Good:

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

프로미스 테스트

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

it('tests a promise', async () => {
  const users = await fetchUsers()
  expect(users.length).toBe(42)
});

it('tests a promise rejection', async () => {
  await expect(user.getUserName(1)).rejects.toThrow('User with 1 not found.');
});

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

프로미스와 관련하여 donedone.fail 콜백을 사용하는 것은 프로미스를 다룰 때는 권장하지 않습니다. 그러한 것들은 사용되어서는 안됩니다.

Bad:

// return이 누락됨
it('tests a promise', () => {
  promise.then(data => {
    expect(data).toBe(asExpected);
  });
});

// done/done.fail를 사용합니다
it('tests a promise', done => {
  promise
    .then(data => {
      expect(data).toBe(asExpected);
    })
    .then(done)
    .catch(done.fail);
});

Good:

// 해결된 프로미스를 확인
it('tests a promise', () => {
  return promise
    .then(data => {
      expect(data).toBe(asExpected);
    });
});

// Jest의 `resolves` 매처를 사용하여 해결된 프로미스를 확인
it('tests a promise', () => {
  return expect(promise).resolves.toBe(asExpected);
});

// Jest의 `rejects` 매처를 사용하여 거부된 프로미스를 확인
it('tests a promise rejection', () => {
  return expect(promise).rejects.toThrow(expectedError);
});

시간 조작

가끔은 시간에 민감한 코드를 테스트해야 할 수도 있습니다. 예를 들어 X 초마다 실행되는 반복적 이벤트 등이 그러한 예입니다. 이를 다루는 몇 가지 전략이 있습니다.

setTimeout() / setInterval()에서 응용 프로그램

응용 프로그램 자체가 어떤 시간을 기다리고 있다면, 대기 상태를 목으로 만드는 것이 좋습니다. Jest에서는 이미 기본적으로 이것이 수행됩니다 (자세한 내용은 Jest 타이머 목을 참조하십시오).

const doSomethingLater = () => {
  setTimeout(() => {
    // do something
  }, 4000);
};

Jest에서:

it('does something', () => {
  doSomethingLater();
  jest.runAllTimers();
  
  expect(something).toBe('done');
});

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을 피해야 합니다. 왜냐하면 이렇게 하면 대기하는 이유가 분명하지 않습니다. 게다가, 테스트에서 흉내를 내기 때문에 사용하기가 까다롭습니다.
  • Jest 27 버전 이후로는 더 이상 지원되지 않기 때문에 setImmediate을 피해야 합니다. 자세한 내용은 이 이픽을 참조하십시오.

Promises and Ajax calls

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 () => {
  동기적인함수();
  
  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 Best Practices

원시 값 비교 시 toEqual 대신 toBe 선호

Jest에는 toBetoEqual 매처가 있습니다. toBe는 값을 비교할 때 Object.is를 사용하므로 기본적으로 toEqual보다 더 빠릅니다. 후자는 결국 복잡한 객체의 비교가 필요한 경우에만 사용해야 합니다.

예시:

const foo = 1;

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

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

보다 적절한 매처 선호

toHaveLength 또는 toBeUndefined와 같은 유용한 매처를 사용하여 테스트 코드를 더 읽기 쉽고 이해하기 쉬운 에러 메시지를 생성하세요. 전체 매처 디렉터리은 여기를 확인하세요.

예시:

const arr = [1, 2];

// 출력:
// Expected length: 1
// Received length: 2
expect(arr).toHaveLength(1);

// 출력:
// Expected: 1
// Received: 2
expect(arr.length).toBe(1);

// 출력:
// expect(received).toBe(expected) // Object.is equality
// Expected: undefined
// Received: "bar"
const foo = 'bar';
expect(foo).toBe(undefined);

// 출력:
// expect(received).toBeUndefined()
// Received: "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);

비결정적 사양 피하기

비결정적 사양은 불안정하고 부서지기 쉬운 사양의 둥지입니다. 이러한 사양은 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();
  
  // 참고: `useFakeDate`는 테스트 실행 중에 호출할 수 없습니다(즉, `it`, `beforeEach`, `beforeAll` 등 내부에서 호출할 수 없습니다).
  describe("on Ada Lovelace's Birthday", () => {
    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 환경에서 모듈을 모킹하는 데 사용됩니다. 이는 테스트 환경에서 쉽게 소비할 수 없는 모듈을 모킹함으로써 단위 테스트를 단순화하는 매우 강력한 테스트 도구입니다.

경고: 매뉴얼 모킹이 모든 사양에 일관적으로 적용되지 않아도 되는 경우(즉, 몇 가지 사양에서만 필요한 경우) 매뉴얼 모킹을 사용하지 마세요. 대신 관련 있는 사양 파일에서 jest.mock(..) (또는 유사한 모킹 함수)을 고려하세요.

매뉴얼 모킹을 어디에 두어야 하나요?

Jest는 소스 모듈 옆에__mocks__/ 디렉터리에 모듈 모킹을 두어 매뉴얼 모듈 모킹을 지원합니다(예: app/assets/javascripts/ide/__mocks__와 같이).

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 - 이 모킹은 모듈 자체가 webpack에서 이해하는 AMD 형식을 사용하기 때문에 모듈 자체가 jest 환경과 호환되지 않지만, 이 모킹은 어떠한 동작도 제거하지 않습니다. 단지 es6 호환 래퍼를 제공합니다.
  • __mocks__/monaco-editor/index.js - 이 모킹은 Monaco 패키지가 Jest 환경에서 완전히 호환되지 않기 때문에 도움이 됩니다. 사실, webpack은 이를 작동시키기 위해 특별한 로더가 필요합니다. 이 모킹은 이 패키지가 Jest에서 사용될 수 있게 합니다.

모킹을 가볍게 유지하세요

글로벌 모킹은 마법을 소개하고 기술적으로 테스트 커버리지를 줄일 수 있습니다. 모킹이 이득이 있다고 판단되는 경우:

  • 모킹을 짧고 집중되게 유지하세요.
  • 해당 모킹에 대한 이유에 대한 최상위 주석을 남기세요.

추가 모킹 기술

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

프론트엔드 테스트 실행

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

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

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

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

테스트 스위트를 작업하는 동안 이 사양을 시청 모드로 실행하여 저장할 때마다 자동으로 다시 실행되도록하려는 경우 이러한 사양을 실행하고자하는 경우에는 다음 명령을 사용하세요.

# 아이콘 이름과 일치하는 모든 사양을 감시하고 다시 실행합니다
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();
  
  // ...
});

픽스처 생성

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

  • 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 패키지 레지스트리에 frontend 픽스처 패키지가 있는지 확인합니다.
#
# 해당 패키지가 있으면 다운로드하고 추출합니다.
$ scripts/frontend/download_fixtures.sh

# 현재 체크아웃한 브랜치의 마지막 10개 커밋만 확인합니다.
$ scripts/frontend/download_fixtures.sh --max-commits=10

# 현재 체크아웃한 브랜치 대신 지역 master 브랜치의 커밋을 확인합니다.
$ scripts/frontend/download_fixtures.sh --branch master

새로운 픽스처 생성

각 픽스처에 대해 output 파일의 내용을 찾을 수 있습니다. 예를 들어, 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 쿼리 픽스처

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는 다음을 위한 데이터 주도 테스트를 지원합니다:

이를 사용하면 테스트 안의 반복을 줄이는 데 유용합니다. 각 옵션은 데이터 값을 나타내는 배열이나 태그가 지정된 템플릿 리터럴을 사용할 수 있습니다.

예를 들어:

// 테스트할 함수
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)
 }
);

참고: 템플릿 리터럴 블록은 예쁜 출력이 필요하지 않을 때에만 사용하세요. 예를 들어, 빈 문자열, 중첩된 객체 등의 경우입니다.

예를 들어, 빈 검색 문자열과 비어 있지 않은 검색 문자열 간의 차이를 테스트하는 경우에는 템플릿 리터럴 블록 구문과 pretty print 옵션을 사용하는 것이 좋습니다. 이렇게 하면 빈 문자열 ('')과 비어 있지 않은 문자열 ('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를 활성화하도록 구성해야 합니다. 그렇지 않으면 모호한 오류 메시지가 표시될 수 있습니다.

RSpec 테스트에서 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가 타임아웃될 수 있습니다. 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';

참고: 테스트 타임아웃을 무시하지 마세요. 실제로 프로덕션 문제가 있다는 신호일 수 있습니다. 이 기회를 활용하여 프로덕션 webpack 번들과 청크를 분석하고 비동기 가져오기로 인해 프로덕션에서 문제가 없는지 확인하세요.

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

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

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

  • 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 모킹 모듈에는 Jest 테스트를 위한 두 개의 도우미 메서드가 포함되어 있습니다. Vue 컴포넌트가 수명 주기의 일환으로 요청을 수행할 때와 같이 요청의 Promise에 핸들을 갖고 있지 않을 때 매우 유용합니다.

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

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

shallowMountExtendedmountExtended

shallowMountExtendedmountExtended 유틸리티를 사용하면 find 또는 findAll로 접두사를 붙여 사용 가능한 DOM Testing Library 쿼리 중 하나를 수행할 수 있습니다.

import { shallowMountExtended } from 'helpers/vue_test_utils_helper';

describe('FooComponent', () => {
  const wrapper = shallowMountExtended({
    template: `
      <div data-testid="gitlab-frontend-stack">
        <p>GitLab frontend stack</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 frontend stack').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

개인의 브라우저에서 나타날 수 있는 여러 가지 문제를 보고하기 위해 다양한 환경에서 GitLab을 실행하는 것은 매우 중요합니다.

특히, GitLab을 사용하여 수정 된 기능 및 업데이트를 테스트하는 특정 브라우저가 있는 경우 개발자는 특정 브라우저에서 GitLab을 테스트함으로써 자체 제품의 문제를 방지할 수 있습니다.


각 섹션 및 예시들이 캡처되었습니다. 계속해서 문서를 번역하시겠습니까?

스냅샷이 어떻게 작동합니까?

스냅샷은 함수 호출의 왼쪽에 있는 것을 테스트할 것을 요청하는 문자열화된 버전입니다. 이는 문자열의 형식을 바꾸는 어떤 종류의 변화라도 결과에 영향을 미칩니다. 이 프로세스는 자동 변환 단계를 위해 직렬 변환기를 활용하여 수행됩니다. 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)를 사용하여 특정 컴포넌트를 찾고 VTU 래퍼에 대해 기본 .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)
})

뿐만 아니라, 컴포넌트에 올바른 속성을 전달한 후 잘못된 가시성을 가지고 있다면, 스냅샷 테스트는 여전히 통과할 것이며, 문제가 있음을 알지 못할 것입니다.

예제 #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 출력의 좋은 예는 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')
})

더 많은 양의 코드일 수 있지만, 이제 우리의 테스트는 GlTable이 내부 구현을 변경하더라도 깨지지 않을 것이며, 협업자 (또는 6개월 후에 자신)에게 리팩터링하거나 테이블에 추가할 때 보존해야 하는 내용을 전달합니다.

스냅샷 찍는 방법

it('이름을 멋지게 표시합니다', () => {
  expect(prettifyName('Homer Simpson')).toMatchSnapshot()
})

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

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

exports[`이름을 멋지게 표시합니다`] = `
Sir Homer Simpson the Third
`

이제 이 테스트를 호출할 때마다, 새로운 스냅샷이 이전에 생성된 버전과 평가될 것입니다. 스냅샷 파일의 내용을 이해하고 주의깊게 취급해야 함을 강조할 필요가 있습니다. 스냅샷은 내용이 너무 많거나 읽기 어려우면 가치를 잃을 것입니다. 이는 합병 요청 리뷰에서 평가하거나 결코 변경되지 않을 것이 보증된 사람이 읽을 수 있는 항목에 스냅샷을 격리시키는 것을 의미합니다.

wrappers 또는 elements에 대해서도 동일한 방식으로 수행할 수 있습니다.

it('컴포넌트를 올바르게 렌더링합니다', () => {
  expect(wrapper).toMatchSnapshot()
  expect(wrapper.element).toMatchSnapshot();
})

위의 테스트는 두 개의 스냅샷을 작성할 것입니다. 코드베이스 안전성을 위해 값이 클 경우에 어떤 스냅샷이 더 많은 가치를 제공하는지 결정하는 것이 중요합니다. 즉, 이 중 한 스냅샷이 변경될 경우 코드베이스에 잠재적인 문제를 강조할까요? 이것은 우리가 모르고 있는 기존 의존성에서 무엇인가 변경된 경우 예기치 않은 변경을 포착하는 데 도움을 줄 수 있습니다.

기능 테스트 시작하기

기능 테스트란

기능 테스트화이트박스 테스트(white-box testing)로도 알려진 테스트로, 브라우저를 생성하고 Capybara 도우미를 사용합니다. 이는 테스트가 다음을 할 수 있음을 의미합니다:

  • 브라우저에서 요소를 찾음.
  • 해당 요소를 클릭함.
  • API를 호출함.

기능 테스트는 실행 비용이 많이 들기 때문에 이 유형의 테스트를 실행하기 전에 정말로 원하는 것인지 확인해야 합니다.

우리의 모든 기능 테스트는 루비로 작성되지만 종종 사용자 인터페이스 기능을 구현하는 JavaScript 엔지니어가 작성합니다. 그래서 다음 섹션은 루비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는 프롭스를 ChildComponent2로 전달합니다.

이 경우에 단위 테스트(Unit Test)를 사용할 수 있습니다:

  • 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 범위를 만들어야 합니다.

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

다른 것은 루비의 모든 것처럼 이것이 사실상 클래스임이 다릅니다. 이는 맨 위에서 필요한 모듈을 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) }

이렇게 새로 생성된 사용자를 보유하는 변수를 만들고 create를 사용할 수 있습니다. 왜냐하면 우리는 spec_helper를 가져왔기 때문입니다.

그러나 이 사용자를 아직 사용하지 않았기 때문에 단순히 변수일 뿐입니다. 따라서 각 사양의 시작 시 인증된 사용자가 있는 것을 확인하기 위해 사용자와 로그인할 수 있습니다.

  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는 자동으로 도우미 경로를 생성하므로 hardcoded된 문자열 대신 이를 사용해야 합니다. 이 경로는 route 모델을 사용하여 생성되므로, 파이프라인으로 이동하려면 다음과 같이 사용합니다:

  visit project_pipeline_path(project, pipeline)

UI를 통해 이동하거나 비동기 호출을 수행하기 전에는 wait_for_requests를 사용하여 계속 진행하기 전에 실행을 기다려야 합니다.

Elements interaction

요소를 찾고 상호 작용하는 다양한 방법이 많이 있습니다. 최선의 실천법에 대해서는 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 문서에서 찾을 수 있습니다.

Assertions

페이지에서 무언가를 단언하려면 실제로 페이지 문서를 자동으로 정의하는 page 변수에 항상 액세스할 수 있습니다. 이겼이기 때문에 page에 선택기 또는 콘텐츠와 같은 특정 컴포넌트가 있는 것을 예상할 수 있습니다. 몇 가지 예는 다음과 같습니다:

  # 버튼 찾기
  expect(page).to have_button('제출 리뷰')
  # 텍스트로 찾기
  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('제출 리뷰')
  # 테스트 케이스가 연이어 기대되는 경우
  # `:aggregate_failures`를 사용하여 그룹화하는 것이 좋습니다.
  it 'shows the issue description and design references', :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

보다 포괄적인 라우터 디렉터리을 feature tests matchers 문서에서 찾을 수 있습니다.

피처 플래그s

기본적으로 모든 feature flag는 YAML 정의나 GDK에서 매뉴얼으로 설정한 플래그에 관계 없이 활성화됩니다. 플래그가 비활성화될 때 테스트하려면 이를 매뉴얼으로 스텁해야 합니다. 이상적으로는 before do 블록에서 이를 수행해야 합니다.

  stub_feature_flags(my_feature_flag: false)

ee feature flag를 스텁하는 경우 다음과 같이 사용합니다:

  stub_licensed_features(my_feature_flag: false)

디버깅

스펙을 실행할 때 접두사 WEBDRIVER_HEADLESS=0으로 실행하여 실제 브라우저를 열 수 있습니다. 그러나 스펙은 빠르게 명령을 수행하고 주변을 둘러볼 시간을 주지 않습니다.

이 문제를 피하려면 Capybara가 실행을 중지해야 할 줄에 binding.pry를 작성할 수 있습니다. 그럼 표준 사용법으로 브라우저 안에 들어가게 됩니다. 특정 요소를 찾을 수 없는 이유를 이해하기 위해 다음을 수행할 수 있습니다:

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

캐피바라가 실행되고 있는 터미널에서 next를 실행하여 테스트를 한 줄씩 실행할 수도 있습니다. 이렇게 하면 문제를 일으킬 수 있는 요소를 확인하기 위해 각 상호 작용을 한 번에 하나씩 확인할 수 있습니다.

GDK에서 실행 시간 향상

Jest 테스트 스위트를 실행할 때 사용하는 워커 수는 기계의 사용 가능한 코어의 60%를 사용하도록 설정되어 있으므로 실행 시간이 더 빨라지지만 메모리 소비량은 더 많아집니다. 이 작업이 어떻게 작동하는지에 대한 추가 벤치마크는 다음 이슈를 참조하십시오.

ChromeDriver 업데이트

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


테스트 문서로 돌아가기