`프론트엔드 개발자` 개형이의 벽돌집

리액트 앱 단위테스트 해보기 (2) - react testing library, jest 본문

테스트

리액트 앱 단위테스트 해보기 (2) - react testing library, jest

개형이 2022. 6. 20. 17:59

지난 포스팅에 이어 테스트코드 작성 방법에 대해 기록해보려고 한다.

 

 


 

 

테스트 코드 Arrange, Act, Assert


🤔  테스트 코드의 작성은 Arrange, Act, Assert 세가지의 세팅을 해준다고 보면 된다.

 

test('loads and displays greeting', async () => {
  // Arrange
  // Act
  // Assert
})

 

먼저 테스트할 컴포넌트 예시 코드를 하나 작성해봤다.

// Hello.jsx
const Hello = () => {
  return (
    <div>
      Hello World
    </div>
  )
}

export default Hello

 

위 컴포넌트에서 Hello World가 제대로 렌더링되는지 테스트하고 싶다. 

 

우선 테스트 코드를 만들어주는데 {name}.test.js 와 같은 네이밍으로 만든다. (위 코드의 테스트코드는 Hello.test.js)

// Hello.test.js
import Hello from './Hello'
import { render, screen } from '@testing-library/react'

test('renders Hello World as a text', () => {
  // Arrange: test data를 셋업
  render(<Hello />)

  // Act: 버튼 클릭에 대한 시뮬레이션 등등 함수, 로직을 실행

  // Assert: 브라우저의 아웃풋을 확인하고 기대값과 비교 (가상 DOM을 확인)
  const helloWorldElement = screen.getByText('Hello World', { exact: false })
    // or screen.getByText('Hello World')
  expect(helloWorldElement).toBeInTheDocument()
})

위 코드의 주석을 보면 알 수 있듯이 테스트 코드는 Arange, Act, Assert의 단계로 작성된다.

Hello World가 제대로 렌더되는지 테스트하는 부분에서 유저의 행동을 테스트하진 않으므로 Act 단계는 생략되었다.

코드에 대한 자세한 설명은 https://brick-house.tistory.com/25 을 참고하자.

 

 

 

테스트 그룹화 describe


🤔  애플리케이션의 사이즈가 커질수록 테스트 파일은 많아지므로 그룹화하여 관리할 필요가 있다.

describe를 활용해보자 ! 

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

// Hello Component라는 그룹(Test Suite)에 하위 테스트가 포함된다.
describe('Hello Component', () => {
  test('renders Hello World as a text', () => {
    // ~~~~~
  })
})

 

 

 

사용자 상호 작용, state 테스트


🤔  '@testing-library/user-event'를 import하여 userEvent 트리거를 테스트할 수 있다.

//...
import userEvent from '@testing-library/user-event'

// 버튼을 클릭했을 경우 changed가 render, 클릭 안했을 경우는 'hello'가 render.

test('renders "hello" if the button was NOT clicked', () => {
  render(<Hello />)

  const outputElement = screen.getByText('hello', { exact: false })
  expect(outputElement).toBeInTheDocument()
})
test('renders "changed" if the button was clicked', () => {
  // Arrange
  render(<Hello />)

  // Act
  const buttonElement = screen.getByRole('button')
  userEvent.click(buttonElement)
    
  // Assert
  const outputElement = screen.getByText('changed')
  expect(outputElement).toBeInTheDocument()
})
test('does not render "hello" if the button was clicked', () => {
  // Arrange
  render(<Hello />)

  // Act
  const buttonElement = screen.getByRole('button')
  userEvent.click(buttonElement) 
    
  // Assert
  const outputElement = screen.queryByText('hello', { exact: false })
  // queryByText를 이용하면 못찾을 경우 null을 반환, 
  // getByText를 사용하면 못찾을 경우 테스트 fail 된다.
  expect(outputElement).toBeNull()
})

- {exact: false} 를 옵션으로 적용하면 정확히 텍스트가 일치하지 않아도 텍스트가 포함된 Element를 찾아 반환한다.

- Hello 컴포넌트에 버튼이 하나밖에 없기 때문에 getByRole('button')을 통해 클릭한 버튼을 가져올 수 있다.

- 클릭했을 경우 changed가 보여야할 뿐만 아니라 hello 텍스트가 보여지면 안되는 부분도 있기 때문에 두 경우 모두 테스트 코드를 넣어준다.

 

 

 

비동기 테스트와 Mock


🤔 비동기 데이터를 가져올 때에는 get대신 find를 사용해야 한다. 우선 테스트할 컴포넌트 예시는 아래와 같다.

// api 호출 결과를 리스트로 보여주는 컴포넌트. 쉽게 이해할 수 있다.
const Async = () => {
  const [data, setData] = useState([]);

  useEffect(() => {
    fetch('api url')
      .then((response) => response.json())
      .then((data) => {
        setData(data);
      });
  }, []);

  return (
    <div>
      <ul>
        {data.map((item) => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
};

 

위 컴포넌트에 리스트가 잘 보여지는지를 테스트하기 위해서는 비동기 테스트가 가능해야 한다.

(데이터를 가져오는데 시간이 걸리기 때문..)

 

비동기를 지원하는 테스트 코드를 작성해보자.

 

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

describe('async and mock', () => {
    test('renders datas if request succeeds', async () => {
        render(<Async />)

        // getAllByRole 을 사용할 경우 즉시 조회하기 때문에 비동기로 데이터를 가져오는 것에 대한 확인은 불가능하다.
        // const listItemElements = screen.getAllByRole('listitem')

        // findAllByRole 을 사용할 경우 프로미스를 반환한다.
        // HTTP 요청이 성공할 때까지 기다리게 되며 세번째 인자에 timeout 기간을 정하여 테스트가 가능 ('listitem', {exact: ~~}, {timeout: ~~ })
        // default timeout은 1초이다.
        const listItemElements = await screen.findAllByRole('listitem') // 프로미스를 반환하므로 앞에 await!
        expect(listItemElements).not.toHaveLength(0)
    })
})

위 코드 주석에서 확인할 수 있듯이 getAllByRole을 사용할 경우는 비동기로 데이터를 가져오는 것에 대한 테스트가 불가능하므로,

findAllByRole (Promise 타입으로 반환)을 사용해서 Element를 가져와야 한다.

 

하지만 위와 같이 테스트를 할 경우 fetch를 통해 서버의 데이터가 변경될 경우도 있기 때문에

위 방식이 최선의 테스트는 아니라고 하는데…

 

이때 사용해야 하는 것이 Mock (모의) 이다.

 

 

😀 모의 작업 (Mock)

테스트를 실행할 때 일반적으로 서버에 HTTP 요청을 전송하지 않아야 한다.

왜?

  1. 네트워크 트래픽을 과도하게 일으켜 서버가 요청들로 인해 과부화가 될 수 있다. (많은 api 테스트가 존재할 경우)
  2. 테스트로 서버에 POST 요청을 일으키거나 하면 서버 데이터가 변경될 가능성이 있다.

mock (더미) function을 작성하여 테스트에 이용하는 방식으로 해결 가능한데 아래 코드를 살펴보자.

 

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

describe('async and mock', () => {
    test('renders datas if request succeeds', async () => {
        // jest.fn 은 mock function을 만들어준다.
        window.fetch = jest.fn()
        window.fetch.mockResolvedValueOnce({
            // json 형태로 response 리턴되도록.
            json: async () => [{id: '01', name: 'num1'}]
        })
        render(<Async />)

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

 

mock을 이용해 더미 데이터를 생성하여 api 요청을 일으키지 않고 리스트 아이템이 렌더링되는지 확인할 수 있다!

 

 

 

 

JEST: https://jestjs.io/docs/getting-started
React-Testing-Library: https://testing-library.com/docs/react-testing-library/intro/
Comments