본문 바로가기
WIKI 기술 지식 베이스

Jest로 테스트

원문 보기 위키 갱신

레거시 문서

출처: 문서

본문

레거시 문서

이 섹션은 레거시 플러그인 문서의 일부예요. 여기 설명된 일반적인 테스트 원칙은 여전히 적용되지만, 시스템별 테스트 가이드는 Testing Frontend Plugins와 Testing Backend Plugins and Modules를 참고하세요.

참고

Jest 30으로 마이그레이션하는 것을 고려할 수 있어요. 이렇게 하려면 Migrating to Jest 30 가이드를 따라할 수 있어요.

Backstage는 모든 단위 테스트 요구에 Jest를 사용해요.

Jest는 Facebook이 React용으로 특별히 만든 단위 테스트 프레임워크예요. Mocha, Jasmine, Chai 같은 다른 고전 Node.js 단위 테스트 관련 프레임워크와 라이브러리의 뒤를 따르는 형태예요.

테스트 실행

모든 테스트 실행:

yarn test

개별 테스트 실행(예: MyComponent.test.tsx):

yarn test MyComponent

MyComponent.test.tsx와 MyControl.test.tsx 두 테스트 스위트를 실행하려면:

yarn test MyComponent MyControl

참고

console.log가 나타나지 않는다면 작업 중인 개별 테스트만 실행하세요. 이는 Jest의 버그예요.

테스트 파일 명명

테스트는 [filename].test.ts로 명명되어야 하며, JSX를 포함하면(많은 React 테스트, 예: 컴포넌트의 경우처럼) [filename].test.tsx로 해야 해요.

예를 들어 Link.tsx에 대한 테스트는 Link.test.tsx 파일에 존재해요.

서드파티 의존성

Jest에는 expect로 자체 내장 단언 라이브러리가 있으므로, 일부 오래된 프레임워크가 요구했던 것처럼 서드파티 라이브러리를 import할 필요가 없어요. 그러나 단언 라이브러리는 단순히 오류를 던지기 때문에 필요하다면 서드파티 라이브러리(Chai나 Sinon 같은)를 import하는 것도 가능해요.

우리는 React 컴포넌트를 렌더링하기 위해 가벼운 react-testing-library를 사용해요.

단위 테스트 작성

다음 원칙들은 고품질 프론트엔드 단위 테스트를 작성하고 있는지 판단하는 좋은 지침이에요.

나쁜 단위 테스트 원칙

나쁜 테스트보다는 없는 것이 낫다 (No unit test is better than a bad one).

좋지 않은 단위 테스트 작성:

  • 코드가 실제보다 더 안전하거나 신뢰할 수 있다는 착각을 준다.
  • 다음 개발자를 잘못된 가정으로 이끈다는 점에서 나쁜 주석과 동등하게 기능한다.
  • 무관한 코드 변경에 대해 단위 테스트를 갱신해야 하므로 미래의 작업을 늘린다.

입출력 원칙

단위 테스트는 출력이 예상 입력과 일치하는지 검증한다.

백엔드의 경우, 구성 X를 제공하면 객체가 Y로 응답해야 함을 뜻한다. 프론트엔드의 경우, 컴포넌트에 속성 X를 제공하면 시각적 기능이 Y로 응답해야 함을 뜻한다.

블랙박스 원칙

좋은 단위 테스트는 객체에 어떻게 그 일을 해야 하는지 말하지 않고 입력을 출력과 비교만 해야 한다.

폼에 대한 단위 테스트를 생각해 보자. 좋은 단위 테스트는 폼 필드의 순서를 테스트하지 않을 것이다. 대신 폼 필드에 대한 입력이 제출을 클릭했을 때 특정 백엔드 호출로 이어지는지 검증할 것이다.

확장성 원칙

단위 테스트 품질은 단위 테스트를 건드리지 않고 얼마나 많은 코드가 변경될 수 있는지에 정비례한다.

이것은 종종 간과된다! 단위 테스트는 코드가 결코 변경되지 않는다는 것을 검증하는 테스트가 아니다. 나쁜 단위 테스트는 코드에 아주 작은 변경을 할 때마다 단위 테스트를 갱신해야 하도록 작성된다. 좋은 단위 테스트 스위트는 코드 작성 방식에 많은 유연성을 허용하여, 원래 단위 테스트를 건드리지 않고도 미래의 리팩토링이 일어날 수 있게 한다.

복잡도 증가 원칙

스위트의 단위 테스트 순서는 최소 특정에서 최대 특정으로 진행되어야 한다.

Jest는 제공된 순서대로 모든 테스트를 실행하며, 제공한 describe() 블록의 깊이와 무관하다. 우리는 이를 사용해 다음 개발자가 깨뜨린 것을 디버깅하는 데 도움이 되는 테스트를 작성할 수 있다.

여기서의 아이디어는 다음 개발자가 단위 테스트를 깨뜨렸다면, 테스트가 깨진 순서로부터 무엇을 고쳐야 하는지 알 수 있어야 한다는 것이다.

예를 들어 좋은 단위 테스트는 출력을 검증하는 테스트 전에 함수의 인자를 검증할 것이다. 이것을 테스트하지 않으면 출력이 잘못되었다고 오류를 던지는 것은 다음 개발자가 객체의 전체 기능을 깨뜨렸다고 생각하게 이끌 수 있으며, 단지 잘못된 입력이 있었음을 알려주지 않는다.

기능 손상 원칙

일반적으로 단위 테스트는 출력이 정확히 어떻게 나타나는지 테스트하지 않고, 기능이 입력 변경에 대해 예상된 일반적인 응답을 가지는지 테스트해야 한다.

이것은 확장성 원칙에 기대며 주로 프론트엔드 개발에 적용된다. 일반적인 지침으로, 프론트엔드는 가능한 최소한의 코드를 건드리면서 UX나 디자인이 변경될 수 있도록 충분히 유연해야 한다. 예를 들어 나쁜 단위 테스트는 호버 시 버튼의 색을 검증할 것이다. 이는 나쁜 단위 테스트인데, 버튼에 조금 다른 색을 테스트하기로 결정하면 단위 테스트가 깨지기 때문이다. 더 나은 단위 테스트는 호버 시 버튼의 CSS 클래스명이 올바르게 할당되는지 검증하거나 완전히 다른 것을 테스트할 것이다.

예시: 로딩 표시기

프론트엔드의 고전적인 단위 테스트는 백엔드 요청이 이루어질 때 로딩 표시기가 표시되는지 검증하는 것이다.

데이터가 로딩 중일 때 테스트할 수 있는 몇 가지가 있다:

컴포넌트의 내부 loading 상태가 변경되었는가? (나쁨)

이것은 좋은 테스트가 아니다. 실제로 기능(사용자에게 메시지 표시)이 일어나는지 테스트하지 않기 때문이다. 또한 컴포넌트의 내부가 특정 방식으로 작동해야 한다고 기대함으로써 블랙박스 원칙을 깨뜨린다. 이것은 그 자체로 좋은 테스트일 수 있지만, 실제로 입력(로딩 데이터)이 발생하면 출력(사용자에게 메시지 표시)이 일어났는지 검증하는 목표는 달성하지 못한다.

"Loading!"이라는 텍스트가 DOM에 나타났는가? (더 좋음)

이것은 기능을 검증하므로 더 나은 테스트이지만, 확장성 원칙을 깨뜨린다. 'Loading...'을 테스트함으로써 테스트 코드를 컴포넌트의 메시지와 연결하고 있다. 국제화를 추가하거나 단순히 메시지를 더 구체적인 것으로 변경하고 싶다면 테스트가 깨지고 두 곳에서 코드를 갱신해야 한다.

<Loading />이 마운트되었는가? (가장 좋음)

이것은 이 예시들 중 가장 좋은 테스트다(구현에 따라 더 있을 수 있다).

데이터가 로딩 중일 때 <Loading />이 마운트되는지 검증하는 것은 모든 위 원칙을 충족하므로 가장 좋은 테스트다.

✓ 입출력 원칙 충족: 입력이 변경될 때 출력이 변경되는지 검증한다. ✓ 블랙박스 원칙 충족: <Loading />이 어떻게 마운트되는지 검증하지 않고, 입력에 대한 응답으로 마운트된다는 것만 검증한다. ✓ 확장성 원칙 충족: 로딩 표시기가 표시된 전체 방식을 리팩토링하기로 결정해도 테스트를 건드리지 않고도 작동한다. ✓ 기능 손상 원칙 충족: 이 테스트는 기능(표시기 표시)이 작동하는지 검증한다. 어떻게 작동하는지가 아니라.

복잡도 증가 원칙은 이 예시에 실제로 적용되지 않으므로 제외했다. 그러나 이 테스트를 다른 테스트 스위트에 두게 된다면, 컴포넌트가 데이터를 로드하라고 지시받았을 때 실제로 로드하는지 먼저 테스트하는 것이 가장 좋다. 이렇게 하면 데이터 로딩 부분이 깨지면 두 테스트 모두 실패하고, 다음 개발자는 문제가 로딩 표시기가 아니라 데이터 로딩이라는 것을 즉시 알 수 있다.

예시

Backstage 백엔드 플러그인과 모듈 또는 프론트엔드 플러그인을 테스트하는 더 구체적인 예시는 다음 가이드를 확인할 수 있다.

  • Testing Backend Plugins and Modules
  • Testing Frontend Plugins

유틸리티 함수

유틸리티 함수는 부작용 없는 함수다. 인자를 받아 결과를 반환하거나 오류나 콘솔 메시지를 표시한다. 이렇게:

StringUtil ellipsis

export function ellipsis(text, maxLength, midCharIx = 0, ellipsis = '...') {  // Do something blackbox. We should not care about the internals,  // only inputs and outputs.  ...  return someFinalValue;}

유틸리티 함수에서 테스트할 네 가지가 있다:

  • 잘못된 입력 처리
  • 기본 입력 인자 검증
  • 예상 입력 인자에 대한 출력 검증
  • 던져진 오류 처리

잘못된 입력 처리 (던져진 오류 처리):

it('Throws an error on improper arguments', () => {  expect(() => {    ellipsis();  }).toThrowError("Expected 'text' to be defined");});

기본 입력 인자 검증:

it('Works with defaults', () => {  expect(ellipsis('Hello world', 3)).toBe('Hel...');  expect(ellipsis('', 3)).toBe('');  expect(ellipsis('H', 3)).toBe('H');  expect(ellipsis('Hello', 5)).toBe('Hello');});

예상 입력 인자에 대한 출력 검증:

이것은 특히 경계 케이스에 해당한다!

it('Works with midCharIx', () => {  expect(ellipsis('Hello world', 3, 6)).toBe('...o w...');  expect(ellipsis('', 3, 6)).toBe('');  expect(ellipsis('Backstage is amazing', 4, 10)).toBe('...e is...');});

React가 아닌 클래스

React 컴포넌트가 아닌 JavaScript 객체를 테스트하는 것은 다른 언어에서 객체를 테스트하는 것과 많은 원칙이 같다.

API 테스트 원칙

API 테스트는 네 가지를 검증하는 것을 포함한다:

  • 잘못된 입력이 서버로 보내지기 전에 잡히는지.
  • 유효한 입력이 유효한 브라우저 요청으로 변환되는지.
  • 서버 응답이 예상된 JavaScript 객체로 변환되는지.
  • 서버 오류가 우아하게 처리되는지.

API 호출 목킹

Jest에서 목킹은 기존 함수(API 호출 함수 같은)를 대안으로 감싸는 것을 포함한다.

예를 들어:

./MyApi.ts

export async function fetchSomethingFromServer() {  // Live production call to a URI. Must be avoided during testing!  return fetch('blah');}

./__mocks__/MyApi.ts

export async function fetchSomethingFromServer() {  // Simulate a production call response  return 'some result object simulating server data here';}

./MyApi.test.ts

// This import will actually return the contents of the file in the// __mocks__ folder now, due to the jest.mock line belowimport { fetchSomethingFromServer } from './MyApi';// This instructs Jest to swap all imports of './MyApi.ts' to// './__mocks__/MyApi.ts' - this gets automatically hoisted to the top// of the filejest.mock('./MyApi');it('loads data', async () => {  await expect(fetchSomethingFromServer()).resolves.toBe(    'some result object simulating server data here',  );});

React 컴포넌트

React 생명주기 작업

React 생명주기는 비동기적이다.

컴포넌트의 setState를 호출하거나 props를 갱신하면, 다시 렌더링되기 전에 몇 가지 비동기 단계가 발생해야 한다. 다음 예시를 주목하자.

class MyComponent extends Component {  load() {    this.setState({loading: true});  }  render() {    return this.state.loading ? <Loading /> : 'Finished!';  }}...// INCORRECTit('Test loading', () => {  const wrapper = mount(<MyComponent />);  wrapper.load();  expect(wrapper.find('Loading').length).toEqual(1); // Will fail});// CORRECTit('Test loading', () => {  const wrapper = mount(<MyComponent />);  wrapper.load();  wrapper.update(); // This tells the components to run through a render cycle  expect(wrapper.find('Loading').length).toEqual(1);});

자세한 내용:

  • React 생명주기

store, theme, 라우팅, 브라우저 기록 등에 접근

Backstage 애플리케이션에는 루트에 여러 핵심 제공자가 있다. 테스트를 "샘플" Backstage 애플리케이션에 감싸서 실행하려면 유틸리티 함수를 사용할 수 있다.

wrapInTestApp

import { wrapInTestApp } from '../../test-utils';...it('Definitely is not a coconut', () => {  const mangoWrapper = mount(wrapInTestApp(<Mango />));  expect(mangoWrapper.context().store).toBeDefined();});

참고: 테스트 애플리케이션에 감싸는 것은 감싼 컴포넌트가 이제 애플리케이션이기 때문에 find() 또는 dive()를 해야 한다.

Jest 테스트 디버깅

여기에서 찾을 수 있다.

더 알아보기 (Learn more)