본문 바로가기

클라이언트/React
[리액트(React)] 유닛 테스트(Unit Test)

// 유닛 테스트, Unit Test

- application의 가장 작은 단위에 대한 테스트를 작성한다.
   ~ functions, components ...
- 기본적으로 테스팅 코드를 실행하고 결과를 확인하기 위해서는 도구가 필요하다. > Jest
- 리액트 앱에서는 리액트 앱과 컴포넌트 렌더링 시뮬레이션도 필요하다.> React Testing Library


// create-react-app
1. 설치

npm install --save-dev jest
npm install --save-dev @testing-library/react

2. 테스트 코드 작성

- 테스팅 파일명은 해당 컴포넌트 파일의 이름 뒤에 .test.js를 붙인다.

test(테스트 설명, () => {
  1. Arrange 
    - test data 세팅
      ~ render(컴포넌트명)
  2. Act
    - 조건 실행(생략 가능)
    ~ const 변수명 = screen.테스트함수('선택자')
      userEvent.함수(변수명)
      
  3. Assert
    - 결과 단언 ( ~하면 성공)
    ~ const 변수명 = screen.테스트함수('확인할 내용', [exact 속성])
      expect(변수명).assert함수
});

 


3. 테스트 실행

npm test


// Test Suite
- 테스트를 그룹화하는 것
- describe 함수로 suite를 생성한다.

describe('suite 설명', () => {
  test();
});


- 여러 개의 suite가 있을 수 있으며, 하나의 suite에 여러 개의 test가 있을 수도 있다.


import { render } from '@testing-library/react';
import Greeting from './Greeting';
import userEvent from '@testing-library/user-event';

describe('Greeting component', () => {
  test('renders Hello World as a text', () => {
    // Arrange
    render(<Greeting />);

    // Act

    // Assert
    const hellowWorldElement = screen.getByText('Hello World', {
      exact: false,
    });
    expect(hellowWorldElement).toBeInTheDocument;
  });

  test('renders good to see you if the button was NOT clicked', () => {
    render(<Greeting />);

    const outputElement = screen.getByText('good to see you', {
      exact: false,
    });
    expect(outputElement).toBeInTheDocument;
  });

  test('renders Changed! if the button was clicked', () => {
    render(<Greeting />);

    const buttonElement = screen.getByRole('button');
    userEvent.click(buttonElement);

    const outputElement = screen.getByText('Changed!');
    expect(outputElement).toBeInTheDocument;
  });

  test('does not render "good to see you" if the button was clicked', () => {
    render(<Greeting />);

    const buttonElement = screen.getByRole('button');
    userEvent.click(buttonElement);

    const outputElement = screen.queryByText('good to see you', {
      exact: false,
    });
    expect(outputElement).toBeNull();
  });
});

// 비동기 test
- 요청을 보내는 함수(fetch..)를 실제로 실행해서는 안 되기 때문에 더미 함수(jest.fn)로 대체해야 한다.
- 요청이 성공했는지를 테스트하는 것이 아니라 요청을 성공했을 때 처리를 잘 하는지를 테스트 해야 한다.


- fetch 버전

import { render, screen } from '@testing-library/react';
import Async from './Async';

describe('Async component', () => {
  test('renders posts if request succeeds', async () => {
    window.fetch = jest.fn();
    window.fetch.mockResolvedValueOnce({
      json: async () => [{ id: 'p1', title: 'First post' }],
    });
    render(<Async />);

    const listItemElements = screen.findAllByRole('listitem');
    expect(listItemElements).not.toHaveLength(0);
  });
});

- axios 버전

import { render, screen } from '@testing-library/react';
import Async from './Async';
import axios from 'axios';

jest.mock('axios');

describe('Async component', () => {
  test('renders posts if request succeeds', async () => {
    axios.get.mockResolvedValueOnce({
      data: [{ id: 'p1', title: 'First post' }],
    });

    render(<Async />);
    
    const listItemElements = await screen.findAllByRole('listitem');
    expect(listItemElements).not.toHaveLength(0);
  });
});

※ getByRole이나 getAllByRole 등 role을 가져올 때 참고 문서: https://www.w3.org/TR/html-aria/#docconformance

ARIA in HTML

Element with contenteditable=true or element without contenteditable attribute whose closest ancestor with a contenteditable attribute has contenteditable="true". Note This is equivalent to the isContentEditable IDL attribute. aria-readonly="false" Authors

www.w3.org


※ find 쿼리들은 Promise를 반환한다.

describe('Async component', () => {
  test('renders posts if request succeeds', async () => {
    render(<Async />);

    const listItemElements = screen.findAllByRole('listitem');
    expect(listItemElements).not.toHaveLength(0);
  });
});