- Vue.js 테스트
- Jest
- 무엇을 테스트할 것인가, 어떻게 테스트할 것인가
- 일반적인 관례
- 비결정적인 사양 피하기
- 팩토리
- Jest를 사용한 모킹 전략
- 프론트엔드 테스트 실행
- 프론트엔드 테스트 픽스처
- 데이터 주도 테스트
- 주의사항
- 프론트엔드 테스트 수준 개요
- 테스트 헬퍼
- 이전 브라우저에서의 테스트
- 스냅샷
- 기능 테스트 시작하기
프론트엔드 테스트 기준 및 스타일 가이드
프론트엔드 코드를 개발하는 동안 GitLab에서는 두 가지 유형의 테스트 스위트를 만납니다. JavaScript 단위 및 통합 테스트에는 Jest를 사용하고, e2e(End-to-End) 통합 테스트에는 RSpec feature 테스트와 Capybara를 사용합니다.
새로운 기능에 대해 단위 및 feature 테스트를 작성해야 합니다. 대부분의 경우, feature 테스트에는 RSpec를 사용해야 합니다. feature 테스트를 시작하는 방법에 대해 자세히 알아보려면 feature 테스트 시작하기를 참조하세요.
버그 수정을 위해 회귀 테스트를 작성하여 향후 재발을 방지해야 합니다.
GitLab에서의 일반적인 테스트 관행에 대한 자세한 내용은 Testing Standards and Style Guidelines 페이지를 참조하세요.
Vue.js 테스트
Vue 컴포넌트 테스트에 대한 안내서가 필요하다면, 이 섹션으로 바로 이동할 수 있습니다.
Jest
우리는 Jest를 사용하여 프론트엔드 단위 및 통합 테스트를 작성합니다.
Jest 테스트는 EE(Enterprise Edition)에서 /spec/frontend
및 /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 클릭 이벤트의 대상이 되어야 하는 요소를 표시할 수 있습니다.
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
계산된 속성을 테스트하는 것은 당연해 보일 것입니다. 그러나 계산된 속성이 metricTypes
의 길이를 반환하는지 테스트하는 것은 Vue 라이브러리 자체를 테스트하는 것입니다. 이렇게 하는 것은 테스트 스위트에 추가되는 것 외에는 가치가 없습니다. 사용자가 상호 작용하는 방식으로 컴포넌트를 테스트하는 것이 더 나은 접근입니다: 렌더링된 템플릿을 확인하는 것입니다.
// 좋지 않음
describe('computed', () => {
describe('hasMetricTypes', () => {
it('메트릭 유형이 존재하면 true를 반환합니다', () => {
factory({ metricTypes });
expect(wrapper.vm.hasMetricTypes).toBe(2);
});
it('메트릭 유형이 존재하지 않으면 true를 반환합니다', () => {
factory();
expect(wrapper.vm.hasMetricTypes).toBe(0);
});
});
});
// 좋음
it('메트릭 유형이 존재하면 드롭다운을 표시합니다', () => {
factory({ metricTypes });
expect(wrapper.findComponent(GlDropdown).exists()).toBe(true);
});
it('메트릭 유형이 존재하지 않으면 드롭다운을 표시하지 않습니다', () => {
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>')
사용자의 흔적을 따르세요
컴포넌트 중심적인 세계에서 단위 테스트와 통합 테스트의 경계는 상당히 모호할 수 있습니다. 제일 중요한 가이드라인은 다음과 같습니다:
- 미래에 깨지지 않게 하기 위해 복잡한 로직을 격리시켜 테스트하는 경우 깨끗한 단위 테스트를 작성하세요
- 그렇지 않으면 사용자의 흐름에 가깝게 스펙을 작성하려고 노력하세요
예를 들어, 생성된 마크업을 사용하여 버튼을 클릭하고 마크업이 그에 따라 변경되는지를 확인하는 것이 좋습니다. 이렇게 하면 테스트는 통과하지만 실제로 사용자 흐름이 깨질 수 있는 chance가 있습니다.
일반적인 관례
우리의 테스트 스위트의 일부로 포함된 몇 가지 일반적인 관례들입니다. 이 안내서에 어긋나는 사항이 발견된 경우, 가능한 한 빨리 수정하는 것이 이상적입니다. 🎉
DOM 요소 질의 방법
테스트에서 DOM 요소를 질의할 때, 고유하고 의미론적으로 해당 요소를 타겟팅하는 것이 가장 좋습니다.
일반적으로 사용자가 실제로 보는 것을 타겺하는 것이 가장 좋습니다. DOM Testing Library를 사용하여 이를 선호적으로 처리합니다.
텍스트로 선택할 때는 byRole
쿼리를 사용하는 것이 가장 좋습니다.
이는 접근성을 강화하는 데 도움이 됩니다. shallowMountExtended
또는 mountExtended
를 사용할 때 findByRole
및 기타 DOM Testing Library 쿼리를 사용할 수 있습니다.
Vue 컴포넌트 단위 테스트를 작성할 때, 자식을 컴포넌트로써 쿼리하는 것이 좋습니다. 이렇게 하면 단위 테스트가 종합적인 값을 다루는 데 중점을 두고 자식 컴포넌트의 동작 복잡성과 싸우는 것이 아니기 때문입니다.
때로는 위의 두 가지 방법 중 어느 것도 적용할 수 없는 경우가 있습니다. 이런 경우, 셀렉터를 단순화하기 위해 테스트 어트리뷰트를 추가하는 것이 최선일 수 있습니다. 가능한 셀렉터들의 디렉터리은 다음과 같습니다:
-
name
과 같은 의미 있는 어트리뷰트(또한name
이 제대로 설정되었는지 확인) -
data-testid
어트리뷰트(@vue/test-utils
유지보수자에 의해 권장)와 선택적으로shallowMountExtended
또는mountExtended
와 함께 사용 - Vue
ref
(@vue/test-utils
를 사용하는 경우)
import { shallowMountExtended } from 'helpers/vue_test_utils_helper'
const wrapper = shallowMountExtended(ExampleComponent);
it('exists', () => {
// Best (특히 통합 테스트에 적합)
wrapper.findByRole('link', { name: /Click Me/i })
wrapper.findByRole('link', { name: 'Click Me' })
wrapper.findByText('Click Me')
wrapper.findByText(/Click Me/i)
// Good (특히 단위 테스트에 적합)
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'});
// Bad
wrapper.find('.js-foo');
wrapper.find('.btn-primary');
});
data-testid
어트리뷰트에는 kebab-case
를 사용해야 합니다.
테스트 목적으로만 .js-*
클래스를 추가하는 것은 권장하지 않습니다. 다른 적합한 옵션이 없는 경우에만 사용하세요.
자식 컴포넌트 질의
@vue/test-utils
를 사용하여 Vue 컴포넌트를 테스트할 때, 다른 접근 방법은 DOM 노드에 대한 질의 대신 자식 컴포넌트에 대한 질의입니다.
이는 테스트 중인 컴포넌트의 예상 동작을 신뢰할 수 있는 단위 테스트가 가능하도록 하기 위해 구현 세부 정보를 커버해야 한다는 것을 가정합니다. 테스트할 컴포넌트의 예상 동작을 zuite작성된 DOM 또는 컴포넌트 쿼리 모두 괜찮습니다.
예시:
it('exists', () => {
wrapper.findComponent(FooComponent);
});
단위 테스트 명명
특정 함수/메서드를 테스트하는 describe 테스트 블록을 작성할 때, 메서드 이름을 describe 블록 이름으로 사용하세요.
나쁨:
describe('#methodName', () => {
it('passes', () => {
expect(true).toEqual(true);
});
});
describe('.methodName', () => {
it('passes', () => {
expect(true).toEqual(true);
});
});
좋음:
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.');
});
테스트 함수에서 프로미스를 반환할 수도 있습니다.
프로미스와 함께 작업할 때 done
및 done.fail
콜백을 사용하는 것은 권장되지 않습니다.
사용해서는 안 됩니다.
나쁨:
// 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);
});
좋음:
// 해결된 프로미스 검증
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);
});
시간 조작
가끔은 시간에 민감한 코드를 테스트해야 할 때가 있습니다. 예를 들어, 일정 시간마다 실행되는 반복 이벤트 등이 있습니다. 이에 대한 처리 방법은 다음과 같습니다:
애플리케이션의 setTimeout()
/ setInterval()
사용
애플리케이션이 시간을 기다리고 있다면, 대기를 모의하여 처리합니다. Jest에서는 이미 기본적으로 기본적으로 수행됩니다 (자세한 내용은 Jest Timer Mocks를 참조하세요).
const doSomethingLater = () => {
setTimeout(() => {
// 작업 수행
}, 4000);
};
Jest에서:
it('작업을 수행합니다', () => {
doSomethingLater();
jest.runAllTimers();
expect(something).toBe('done');
});
Jest에서 현재 위치 모의
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 버전 이후로 더 이상 지원되지 않습니다. 자세한 내용은 이 Epic을 참조하세요.
프로미스와 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: '새로운 값' });
await nextTick();
expect(wrapper.text()).toBe('새로운 값');
});
이벤트
애플리케이션이 특정 이벤트를 트리거하고 테스트에서 해당 이벤트를 기다려야 하는 경우, 담긴 어설션을 포함하는 이벤트 핸들러를 등록하세요.
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)
}
}
이러한 호출에 대한 테스트 케이스를 작성할 때 매개변수가 올바르게 호출되었는지 확인하기 위해 리졸버를 사용할 수 있습니다.
예를 들어, wrapper를 생성할 때 리졸버가 쿼리 또는 뮤테이션에 매핑되도록 확인해야 합니다.
여기서 모의하는 뮤테이션이 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 최선의 방법
- GitLab 13.2에서 소개되었습니다.
기본 값 비교 시 toEqual
대신 toBe
선호
Jest는 toBe
와
toEqual
매처를 제공합니다.
toEqual
대신 toBe
를 사용하는 것이 더 빠르기(default에 따라) 때문에
값을 비교할 때 toBe
를 선호해야 합니다.
후자는 궁극적으로는 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(받음).toBe(기대) // Object.is 동일성
// 기대: undefined
// 받음: "bar"
const foo = 'bar';
expect(foo).toBe(undefined);
// 출력:
// expect(받음).toBeUndefined()
// 받음: "bar"
const foo = 'bar';
expect(foo).toBeUndefined();
toBeTruthy
또는 toBeFalsy
사용 지양
Jest는 또한 toBeTruthy
와 toBeFalsy
매처를 제공합니다. 해당 매처들은 테스트를 약하게 만들고 거짓 긍정 결과를 만들기 때문에 사용해서는 안 됩니다.
예를 들어, expect(someBoolean).toBeFalsy()
는 someBoolean === null
일 때와 someBoolean === false
일 때 통과합니다.
tricky toBeDefined
매처
Jest에는 false positive 테스트를 생성할 수 있는 tricky toBeDefined
매처가 있습니다. 이는 주어진 값에 대해 undefined
를 유효성 검사하기 때문에 발생합니다.
// 나쁨: finder가 null을 반환하면 테스트가 통과됩니다
expect(wrapper.find('foo')).toBeDefined();
// 좋음
expect(wrapper.find('foo').exists()).toBe(true);
setImmediate
사용 지양
setImmediate
사용을 피하려고 노력하세요. setImmediate
는 I/O가 완료된 후 콜백을 실행하기위한 임시 솔루션입니다. 그리고 이것은 Web API의 일부가 아니므로, 우리는 단위 테스트에서 NodeJS 환경을 대상으로 합니다.
setImmediate
대신 보류 중인 타이머를 실행하는 jest.runAllTimers
또는 jest.runOnlyPendingTimers
를 사용하세요. 후자는 코드에 setInterval
이 있는 경우 유용합니다. 기억하세요: 우리의 Jest 구성은 가짜 타이머를 사용합니다.
비결정적인 사양 피하기
비결정적 사양은 부서지기 쉽고 취약한 사양의 둥지입니다. 이러한 사양은 CI 파이프라인을 망가뜨리고 다른 기여자들의 작업 흐름을 중단시킵니다.
- 테스트 대상의 협업자(예: Axios, Apollo, Lodash 도우미)와 테스트 환경(예: Date)이 시스템 및 시간을 통해 일관되게 동작하는지 확인하세요.
- 테스트가 집중되어 있고 “여분의 작업”을 수행하지 않는지 확인하세요(예: 개별 테스트에서 테스트 대상을 필요 이상으로 여러 번 만드는 경우).
결정을 위한 Date
가짜 만들기
기본적으로 Jest 환경에서 Date
가 가짜 처리되어, Date()
또는 Date.now()
호출이 고정된 결정적인 값을 반환하도록 됩니다.
기본 가짜 날짜를 실제로 변경해야하는 경우에는 useFakeDate
를 describe
블록 내에서 호출하여 해당 describe
컨텍스트 내에서 해당 스펙에 대해 날짜가 교체됩니다.
예시:
import { useFakeDate } from 'helpers/fake_date';
describe('cool/component', () => {
// 기본 가짜 `Date`
const TODAY = new Date();
// 참고: 테스트 실행 중에 `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
클래스를 사용해야하는 경우에는 useRealDate
를 호출할 수 있습니다. 단, useRealDate
는 describe
블록 내에서만 호출할 수 있습니다:
import { useRealDate } from 'helpers/fake_date';
// 참고: `useRealDate`는 테스트 실행 중에 `it`, `beforeEach`, `beforeAll` 내부에서 호출할 수 없습니다.
describe('실제 날짜로', () => {
useRealDate();
});
결정을 위한 Math.random
가짜 만들기
테스트 대상이 Math.random
에 의존하는 경우, Math.random
을 가짜 값으로 대체하는 것을 고려해보세요.
beforeEach(() => {
// https://xkcd.com/221/
jest.spyOn(Math, 'random').mockReturnValue(0.4);
});
팩토리
공개 예정
Jest를 사용한 모킹 전략
스텁 및 모킹
스텁 또는 스파이는 종종 동의어로 사용됩니다. Jest에서는 .spyOn
메서드 덕분에 쉽습니다.
공식 문서를 참조하세요. 더 어려운 부분은 함수 또는 의존성에 대해 사용할 수 있는 모킹입니다.
매뉴얼 모듈 모킹
매뉴얼 모킹은 전체 Jest 환경에서 모듈을 모의화하는 데 사용됩니다. 이것은 우리의 테스트 환경에서 쉽게 사용할 수 없는 모듈을 모의화하여 단위 테스트를 간소화하는 매우 강력한 테스트 도구입니다.
경고: 매뉴얼 모킹이 모든 사양에 일관적으로 적용되어서는 안되는 경우(즉, 몇 가지 사양에서만 필요한 경우)에는 매뉴얼 모킹을 사용하지 마십시오. 그 대신 관련 사양 파일에서
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 형식을 사용하지만 jest 환경과 호환되지 않습니다. 이 모의는 어떤 동작도 제거하지 않고 새로운 ES6 호환 래퍼를 제공합니다. -
__mocks__/monaco-editor/index.js
- 이 목업은 Monaco 패키지가 Jest 환경에서 완전히 호환되지 않기 때문에 유용합니다. 실제로 웹팩은 이를 작동시키기 위해 특별한 로더가 필요합니다. 이 목업은 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('request를 수행합니다', () => {
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
# 현재 체크아웃된 브랜치의 마지막 10개의 커밋을 확인하지 않고 로컬 마스터 브랜치의 커밋을 보겠어요
$ scripts/frontend/download_fixtures.sh --branch master
새로운 픽스처 생성
각 픽스처에 대한 response
변수 내용을 출력 파일에서 찾을 수 있습니다.
예를 들어, (ee/)spec/fixtures/merge_requests.rb
파일의 이름이 "merge_requests/diff_discussion.json"
인 테스트는 tmp/tests/frontend/fixtures-ee/merge_requests/diff_discussion.json
출력 파일을 생성합니다.
response
변수는 type: :request
또는 type: :controller
로 표시된 테스트의 경우 자동으로 설정됩니다.
새로운 픽스처를 생성할 때, 해당 엔드포인트의 테스트를 확인하는 것이 유용합니다.
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
에 위치합니다.
test_fixtures
별칭을 사용하여 Jest 테스트에서 JSON 픽스처를 가져올 수 있습니다
(이전에 설명한 대로)
#use-fixtures.
데이터 주도 테스트
RSpec의 매개변수화 테스트와 유사하게, Jest는 다음을 위한 데이터 주도 테스트를 지원합니다:
-
test.each
를 사용한 개별 테스트 (alias: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)
}
);
''
)과 비어 있지 않은 문자열('검색 문자열'
) 사이의 차이점이 스펙 출력에서 보입니다. 그에 비해 템플릿 리터럴 블록을 사용하면 빈 문자열이 공백으로 표시되어 혼란스러운 개발자 경험을 유발할 수 있습니다.// 나쁜 예
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 '항상 true를 반환한다', :js do
# 단언문...
end
describe "Admin::AbuseReports", :js do
it '항상 true를 반환한다' do
# 단언문...
end
it '항상 true를 반환한다' do
# 단언문...
end
end
비동기 가져오기로 인한 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
공식 문서에 따르면, 액션을 테스트하는 것을 더 쉽게 만들어주는 헬퍼가 있습니다.
// prefer using like this, a single object argument so parameters are obvious from reading the test
await testAction({
action: actions.actionName,
payload: { deleteListId: 1 },
state: { lists: [1, 2, 3] },
expectedMutations: [ { type: types.MUTATION} ],
expectedActions: [],
});
// old way, don't do this for new tests
testAction(
actions.actionName, // action
{ }, // params to be passed to action
state, // state
[
{ type: types.MUTATION},
{ type: types.MUTATION_1, payload: {}},
], // mutations committed
[
{ type: 'actionName', payload: {}},
{ type: 'actionName1', payload: {}},
] // actions dispatched
done,
);
Axios 요청 완료 대기
spec/frontend/__helpers__/mocks/axios_utils.js
에 위치한 Axios Utils mock 모듈에는 Jest 테스트에서 HTTP 요청을 생성하는 데 도움이 되는 두 가지 헬퍼 메서드가 있습니다.
이러한 헬퍼 메서드는 .then()
이나 .catch()
핸들러가 실행되도록 하기 위해 요청 완료 후 콜백을 다음 틱에서 실행합니다 (setImmediate()
를 사용).
-
waitFor(url, callback)
:url
로의 요청이 완료된 후callback
을 실행합니다 (성공 또는 실패에 상관없이). -
waitForAll(callback)
: 보류 중인 요청이 모두 완료될 때callback
을 실행합니다. 보류 중인 요청이 없는 경우,callback
을 다음 틱에서 실행합니다.
shallowMountExtended
및 mountExtended
shallowMountExtended
및 mountExtended
유틸리티는 사용 가능한 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 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('finds elements with `findByTestId`', () => {
expect(wrapper.findByTestId('gitlab-frontend-stack').exists()).toBe(true);
});
it('finds elements with `findByText`', () => {
expect(wrapper.findByText('GitLab frontend stack').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에 로그인할 수 있습니다.
Firefox
macOS
릴리스 FTP 서버 https://ftp.mozilla.org/pub/firefox/releases/에서 Firefox의 이전 버전을 다운로드할 수 있습니다.
- 웹 사이트에서 버전을 선택합니다. 여기서는
50.0.1
를 선택하겠습니다. - mac 폴더로 이동합니다.
- 원하는 언어를 선택합니다. DMG 패키지가 내부에 있습니다. 이것을 다운로드합니다.
- 애플리케이션을
Applications
폴더 이외의 다른 폴더로 끌어다 놓습니다. - 애플리케이션의 이름을
Firefox_Old
와 같은 이름으로 변경합니다. - 애플리케이션을
Applications
폴더로 이동합니다. - 터미널을 열고
/Applications/Firefox_Old.app/Contents/MacOS/firefox-bin -profilemanager
를 실행하여 해당 Firefox 버전에 특정 프로필을 생성합니다. - 프로필을 생성한 후 앱을 종료하고, 보통대로 다시 실행합니다. 이제 작동하는 이전 버전의 Firefox를 사용할 수 있습니다.
스냅샷
Jest 스냅샷 테스트는 주어진 컴포넌트의 HTML 출력에 예상치 못한 변경을 방지하는 유용한 방법입니다. 이를 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 - 요소 가시성
요소 가시성을 테스트할 때는 특정 컴포넌트를 찾고 VTU(wrapper) 래퍼에 있는 기본 .exists()
메소드를 호출하는 vue-tests-utils (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)
})
뿐만 아니라, 예를 들어 컴포넌트에 잘못된 프롭을 전달하고 잘못된 가시성을 가졌을 때, 여전히 스냅샷 테스트는 통과될 것이기 때문에 스냅샷의 출력을 두 번 체크하지 않는 한 테스트가 망가졌다는 것을 알 수 없을 것입니다.
예시 #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('예상대로 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('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
`
이제 이 테스트를 호출할 때마다 새로운 스냅샷이 이전에 생성된 버전과 평가됩니다. 이는 스냫샷 파일의 내용을 이해하고 주의 깊게 취급하는 것이 중요함을 강조해야 합니다. 스냅샷은 출력이 너무 많거나 복잡하여 읽기 어렵다면 가치를 잃게 됩니다. 이는 스냅샷을 인간이 읽을 수 있는 항목으로 격리시키거나 절대 변경되지 않음이 보장된 항목에 보관하는 것을 의미합니다.
wrappers
나 elements
에 대해 동일한 작업을 할 수 있습니다.
it('renders the component correctly', () => {
expect(wrapper).toMatchSnapshot()
expect(wrapper.element).toMatchSnapshot();
})
The 위의 테스트는 두 개의 스냅샷을 생성합니다. 코드베이스의 안전성을 제공하는 더 많은 가치를 제공하는 스샅샷을 결정하는 것이 중요합니다. 다시 말해, 이러한 스냅샷 중 하나가 변경되면 코드베이스에서 잠재적인 손상을 나타내는지를 강조해주는 것입니다. 이는 우리의 지식없이 기본 의존성 내부에서 변경 사항이 있을 때 예기치 않은 변경 사항을 잡을 수 있도록 도와줄 수 있습니다.
기능 테스트 시작하기
기능 테스트란
기능 테스트 또는 화이트박스 테스트
로도 알려진 기능 테스트는 브라우저를 시작하고 Capybara 도우미가 있는 테스트입니다. 이는 테스트가 다음을 할 수 있음을 의미합니다:
- 브라우저에서 요소를 찾을 수 있음.
- 해당 요소를 클릭할 수 있음.
- API를 호출할 수 있음.
기능 테스트는 비용이 많이 듭니다. 이 유형의 테스트를 실행하기 전에 정말로 원하는지 확인해야 합니다.
우리의 모든 기능 테스트는 Ruby
로 작성되지만 종종 JavaScript
엔지니어가 작성하게 됩니다. 따라서 다음 섹션은 Ruby
나 Capybara
에 대한 사전 지식이 없다고 가정하며, 이러한 테스트를 언제 어떻게 사용해야 하는지에 대한 명확한 지침을 제공합니다.
기능 테스트를 사용하는 경우
테스트가 다음과 같은 경우에 기능 테스트를 사용해야 합니다:
- 여러 컴포넌트를 걸쳐 테스트하는 경우.
- 사용자가 페이지를 이동해야 하는 경우.
- 양식을 제출하고 다른 곳에서 결과를 관찰해야 하는 경우.
- 단위 테스트로 진행되었다면 가짜 데이터와 컴포넌트를 많이 모의하고 스텁화해야 하는 경우.
기능 테스트는 특히 다음을 테스트하려는 경우 유용합니다:
- 여러 컴포넌트가 성공적으로 함께 작동하는지.
- 복잡한 API 상호작용. 기능 테스트는 API와 상호 작용하므로 느리지만 어떠한 수준의 모의화나 픽스처도 필요하지 않습니다.
기능 테스트를 사용하지 않아야 하는 경우
기능 테스트는 사용이 비용이 많이 듭니다. 동일한 테스트 결과를 얻을 수 있는 경우에는 대신 jest
및 vue-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
은 이벤트를 방출합니다. -
ParentComponent
는internalData
값을 변경합니다. -
ParentComponent
는 props를childComponent2
로 전달합니다.
이러한 경우, ParentComponent
의 단위 테스트 파일 내에서 예상되는 이벤트를 childComponent1
에서 방출하고, prop이 childComponent2
로 전달되는지 확인할 수 있습니다.
각 자식 컴포넌트는 이벤트가 방출됐을 때와 prop이 변경됐을 때의 동작을 테스트합니다.
이 예는 더 큰 규모와 더 깊은 구성 트리에서도 적용됩니다. 원하는 테스트 동작을 얻을 수 있다면 단위 테스트를 사용하고 추가 비용을 피하는 것이 확실히 가치가 있습니다.
- 자신있게 자식 컴포넌트를 마운트할 수 있습니다.
- 가상 DOM에서 이벤트를 방출하거나 요소를 선택할 수 있습니다.
- 원하는 동작을 테스트할 수 있습니다.
테스트를 생성할 위치
기능 테스트는 spec/features
폴더에 있습니다. 테스트할 페이지가 추가되는 경우 해당 페이지를 테스트할 수 있는 기존 파일을 찾아야 합니다. 그 폴더 안에서 당신의 섹션을 찾을 수 있습니다. 예를 들어, 파이프라인 페이지에 대한 새로운 기능 테스트를 추가하려면 spec/features/projects/pipelines
를 찾아보세요.
기능 테스트 실행 방법
- 작동하는 GDK 환경이 있는지 확인하세요.
-
gdk start
명령으로gdk
환경을 시작합니다. - 터미널에서 다음을 실행합니다.
bundle exec rspec path/to/file:line_of_my_test
이 명령어 앞에 WEBDRIVER_HEADLESS=0
을 붙여 실제로 볼 수 있는 브라우저에서 테스트를 실행하도록 할 수도 있는데, 이는 디버깅에 매우 유용합니다.
테스트 작성 방법
기본 파일 구조
- 모든 문자열 리터럴을 변경할 수 없게 만듭니다.
모든 기능 테스트에서 가장 첫 번째 줄은 다음과 같아야합니다:
# frozen_string_literal: true
이는 모든 Ruby
파일에 있는 것이며 일부 성능상의 이점도 있지만, 이것은 이 섹션의 범위를 벗어납니다.
- 의존성 가져오기.
필요한 모듈을 가져와야 합니다. 대부분의 경우 spec_helper
를 요구할 필요가 있습니다:
require 'spec_helper'
다른 관련 모듈을 가져옵니다.
- RSpec가 우리의 테스트를 정의하기 위한 전역 범위를 만듭니다. 이는 우리가 jest에서 처럼 처음에 describe 블록을 만드는 것과 같습니다.
그런 다음 매우 처음의 RSpec
범위를 만들어야합니다.
RSpec.describe 'Pipeline', :js do
...
end
그렇지만 다른 점은 모든 Ruby와 마찬가지로, 이는 사실상 class
입니다. 즉, 파일 맨 위에서 당신이 필요로 하는 모듈을 include
할 수 있습니다. 예를 들어, 곧 할 것입니다.
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')
필요한 많은 팩토리가 이미 존재하므로 필요한 것이 있는지 확인해야 합니다.
네비게이션
t_visit
메서드를 사용하여 페이지로 이동할 수 있습니다. 페이지 헬퍼 경로가 자동으로 생성되므로 하드코딩된 문자열 대신 이를 사용해야 합니다. 라우트 모델을 사용하여 생성되므로, 예를 들어 파이프라인으로 이동하려면 다음을 사용합니다:
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
을 사용할 수 있습니다.
피처 테스트 작업 문서에서 더 포괄적인 작업 디렉터리을 찾을 수 있습니다.
단언
페이지에서 어떤 것이든 단언하려면, 해당하는 구성요소(셀렉터 또는 콘텐츠)가 포함되어 있는지에 대해 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"]')
더 많은 일치 항목은 피처 테스트 매처 문서에서 찾을 수 있습니다.
피처 플래그
기본적으로 모든 피처 플래그는 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를 매뉴얼으로 동기화할 필요가 없습니다.