예시로 배우는 React 테스팅

예시로 배우는 React 테스팅

Tags
테스팅
Published
February 11, 2024
Author
Seongbin Kim
💡
이론 설명 → 처음부터 테스트 코드 템플릿 → <2-2. 테스트 템플릿>
 
 

1. 테스트를 하는 이유

 

1-1. 버그의 재발 시 알람

버그가 발생하는 예외 상황을 재현하는 TC를 만들면 향후 재발했을 때 즉시 확인할 수 있어요.
 

1-2. 기능 명세

예외 상황 뿐 아니라 정상 상황도 테스트하면 기능 명세가 돼요.
TC를 보면 각 상황 별 동작을 알 수 있어요.
특정 상황이 대비되었는지도 알 수 있어요.
 

1-3. 기능 동작의 안정성

테스트는 쌓일수록 코드가 견고해져요.
리팩토링 후 테스트 결과가 정상이라면 기존 기능이 깨지지 않았음을 확인할 수 있어요.
 

2. 테스트를 하는 방법

 

2-1. 테스트 코드 기본 개념

테스트 코드를 작성할 때는 대상에게 입력을 주고 출력을 확인해요.
 
알고리즘 테스트의 커스텀 TC를 만들듯이 하면 돼요.
  • 예: sum 함수
    • 함수의 경우 인자를 전달하고 호출 결과를 확인하면 되어서 가장 쉬운 대상이에요.
      • const sum = (a, b) => a + b; const input = [ 1, 2 ]; const output = 3; if (sum(...input) !== output) { throw new Error("1+2=3 TC 실패"); }
 

2-1-1. 대상이 컴포넌트, 페이지, 애플리케이션 자체인 경우

전달하고 확인하기가 조금씩 더 어려워져요.
특정 상황을 재현하기 위해서 직접 렌더링을 하는 등의 도구가 필요하기 때문에요.
jsdom
jsdomUpdated Sep 7, 2024
 

2-2. 테스트 종류 3가지

테스트는 크게 3종류가 있어요.
  • 단위 테스트: 최소 단위 코드(컴포넌트 등)
  • 통합 테스트: 의미있는 기능 단위로 합체된 코드(모듈 등)
  • E2E 테스트: 온전한 애플리케이션
 
각 테스트 방법마다 테스트 코드가 달라요.
 

3. 각 테스트 별 예시 상황과 테스트 코드

 
코드 예시가 좋은 글을 찾아서 참고해보시면 좋을 것 같아요
 

3-1. 단위 테스트

  • 단일 컴포넌트만 따로 렌더링해서 동작을 검증한다면 단위 테스트에요.
    • 외부 의존성이 있는 경우에 직접 주입해서 해결하는 경우가 많아요.
      • Context.Provider를 직접 렌더링하는 등
describe('MyComponent 단위 테스트', () => { it('렌더링할 수 있다', () => { const { getByText } = render(<MyComponent />); const linkElement = getByText(/Hello World/i); expect(linkElement).toBeInTheDocument(); }); it('버튼을 클릭하면 카운터가 증가한다', () => { const { getByText, getByTestId } = render(<MyComponent />); const countElement = getByTestId('count'); const buttonElement = getByText(/Click me/i); expect(countElement).toHaveTextContent('0'); fireEvent.click(buttonElement); expect(countElement).toHaveTextContent('1'); }); });
 

3-2. 통합 테스트

  • 특정 페이지만 따로 렌더링해서 동작을 검증한다면 통합 테스트에요.
    • 통합 테스트는 외부 의존성을 mock해요.
      • 실제 API, DB를 써도 되지만 보통 mock하는 게 더 편해요.
        • 따라서 실제로 요청이 발생하지 않게 만드는 경우가 많아요.
describe('검색페이지 통합테스트', () => { it('검색어 입력후 엔터 키를 입력하면 검색된 앨범을 보여준다.', async () => { render(<Search />); const inputBox = screen.getByRole('textbox'); //검색어 입력 fireEvent.change(inputBox, { target: { value: '아이유' } }); //API 호출 fireEvent.keyDown(inputBox, { key: 'Enter' }); await waitFor(() => { //렌더링 결과 확인 expect(screen.queryByText('조각집')).toBeInTheDocument(); }); }); it('텅빈 데이터를 응답받는 경우 안내문을 출력한다.', async () => { /** * 위에서와 동일한 방법으로 Empty 데이터를 처리해 줍니다. */ overrideSearchResultWithEmptyData(); render(<Search />); const inputBox = screen.getByRole('textbox'); fireEvent.change(inputBox, { target: { value: '아이유' } }); fireEvent.keyDown(inputBox, { key: 'Enter' }); await waitFor(() => { expect(screen.queryByText('검색 결과가 없습니다.')).toBeInTheDocument(); }); }); it('네트워크 에러가 발생하면 안내문을 출력한다.', async () => { overrideSearchResultWithErrorData(); render(<Search />); const inputBox = screen.getByRole('textbox'); fireEvent.change(inputBox, { target: { value: '아이유' } }); fireEvent.keyDown(inputBox, { key: 'Enter' }); await waitFor(() => { expect(screen.queryByText('검색 결과가 없습니다.')).toBeInTheDocument(); }); }); }); // API 응답 mocking 코드 function overrideSearchResultWithErrorData() { server.use( rest.get('/search', (_, res, ctx) => res(ctx.status(404), ctx.json({ errorMessage: 'Not Found' })), ), ); }
 

3-3. E2E 테스트

  • 브라우저로 웹 페이지에 접속해 Copyright가 잘 나오는지 검증한다면 E2E 테스트에요.
import { expect, Page, test } from '@playwright/test'; test.describe('하이퍼커넥트 기술블로그 테스트', () => { let page: Page; test.beforeAll(async ({ browser, contextOptions }) => { // 페이지 생성 const browserContext = await browser.newContext(contextOptions); page = await browserContext.newPage(); // 기술블로그 링크로 이동 await page.goto('https://hyperconnect.github.io/'); }); test('footer의 copyright가 올바르다', async () => { // footer element를 가져옴 const copyrightFooter = await page.locator('body > footer > div > div'); // 올바른 copyright를 계산 const currentYear = new Date().getFullYear(); const validCopyright = `© 2013-${currentYear} Hyperconnect Inc.`; // footer의 text가 올바른 copyright인지 확인 await expect(copyrightFooter).toHaveText(validCopyright); }); });
 

4. 테스트 템플릿

 

4-1. Given-When-Then

⚠️
WIP
describe('사용자는 Todo를 생성할 수 있다', () => { it('제목을 입력하면 Todo를 생성할 수 있다', () => { // Given const // When // Then }); });
 
 

5. 테스팅 스택

 

5-1. Storybook + testing-library

 

5-1-1. 문제점: Storybook과 단위 테스트 간의 코드 중복

Storybook을 사용한다면 컴포넌트를 상태 별로 재현하는 ‘Story’를 작성하게 돼요.
이 때 Storybook과는 별도의 단위 테스트를 작성하게 되면 ‘Story’ 만큼의 코드 중복이 발생해요.
 

5-1-2. 해결책: 렌더링은 Story로, 검증은 단위 테스트로 (구 storybook/testing-react)

이렇게 재현된 화면 구성을 그대로 testing-library 기반의 테스트 코드로 가져올 수 있어요.
 
이렇게 되면 Storybook에서 렌더링까지는 담당하고, 검증은 단위 테스트에서 이어서 할 수 있어요.
import { fireEvent, render, screen } from '@testing-library/react'; import { composeStory } from '@storybook/react'; import Meta, { ValidForm as ValidFormStory } from './LoginForm.stories'; const FormOK = composeStory(ValidFormStory, Meta); test('Validates form', () => { render(<FormOK />); const buttonElement = screen.getByRole('button', { name: 'Submit', }); fireEvent.click(buttonElement); const isFormValid = screen.getByLabelText('invalid-form'); expect(isFormValid).not.toBeInTheDocument(); });
 

5-2. Cypress

⚠️
WIP