[testing-library] react-testing-library (unit test)

dev stefanCho·2021년 12월 11일
0

tdd

목록 보기
3/4

testing-library


@testing-library

아래 3가지는 cra를 하면 기본으로 설치됨

    "@testing-library/jest-dom": "^5.11.4",
    "@testing-library/react": "^11.1.0",
    "@testing-library/user-event": "^12.1.10",

directory 구조

  • 각 컴포넌트 directory내에 __test__ directory를 생성
  • __test__/ComponentName.test.js 생성

Test Block 작성 순서

  • render : 테스트할 컴포넌트를 랜더링 (Render a component we to test)
  • getBy, findBy, queryBy : interact할 Element를 찾음 (Find elements we want to interact with)
  • fireEvent : 찾은 element간에 interation을 테스트 (Interact with those elements)
  • expect : 원하는 결과가 나오는 지 확인 (Assert that the results are as expected)

screen method

screen의 method는 아래와 같이 정리된다.

  • 초록색은 pass하는 경우를 의미한다.
  • 빨간색은 fail하는 경우를 의미한다.
  • async/await를 사용할 수 있는 method는 findBy, findAllBy이다.

screen.getByTestId

getByTestId는 element에 data-testid를 준것에 대해서 찾을 수 있다.

expect().toBeVisible

사용자에게 visible한지 test하기
toBeVisible을 사용
opacity로 간단하게 테스트할 수 있다.

  expect(paragraphElement).toBeVisible(); // 보이는 상태라면
  expect(paragraphElement).not.toBeVisible(); // 보이지 않는 상태라면

expect().toContainHTML

toContainHTML
특정 html이 포함되어 있는지

  expect(paragraphElement).toContainHTML('p');

1개의 테스트에는 1개의 expect assertion만 있도록 한다. (여러개가 있어도 되지만, 에러가 발생하는 원인을 찾기 어렵게 만든다.)

describe

describe로 test block을 만들 수 있음



Error case


테스트할 TodoFooter.js는 아래와 같다.

import React from 'react'
import "./TodoFooter.css"
import { Link } from "react-router-dom"

function TodoFooter({
    numberOfIncompleteTasks
}) {
    return (
        <div className="todo-footer">
            <p>{numberOfIncompleteTasks} {numberOfIncompleteTasks === 1 ? "task" : "tasks"} left</p>
            <Link to="/followers">Followers</Link>
        </div>
    )
}

export default TodoFooter

props가 제대로 넘어가는 지, 그리고 text가 제대로 나오는지 확인하기 위한 테스트를 작성한다.

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

it('should render the correct amount of incomplete tasks', async () => {
  render(<TodoFooter numberOfIncompleteTasks={5} />)
  const paragraphElement = screen.getByText(/5 tasks left/i);
  expect(paragraphElement).toBeInTheDocument();
  expect(paragraphElement).toBeTruthy();
})
// Invariant failed: You should not use <Link> outside a <Router> 에러가 발생한다.

TodoFooter는 기존에는 index.js에서 BrowserRouter에 의해 감싸져 있다.

// index.js
import { BrowserRouter } from "react-router-dom"

ReactDOM.render(
  <BrowserRouter>
    <React.StrictMode>
      <App />
    </React.StrictMode>
  </BrowserRouter>,
  document.getElementById('root')
);

하지만, unit test에서 TodoFooter는 독립적인 컴포넌트이다. 따라서 Link 에러가 발생한다.

import { render, screen } from '@testing-library/react';
import TodoFooter from '../TodoFooter'
import { BrowserRouter } from 'react-router-dom';

const MockTodoFooter = ({ numberOfIncompleteTasks }) => {
  return (
    <BrowserRouter>
      <TodoFooter numberOfIncompleteTasks={numberOfIncompleteTasks} />
    </BrowserRouter>
  )
}

it('should render the correct amount of incomplete tasks', async () => {
  render(<MockTodoFooter numberOfIncompleteTasks={5} />)
  const paragraphElement = screen.getByText(/5 tasks left/i);
  expect(paragraphElement).toBeInTheDocument();
  expect(paragraphElement).toBeTruthy();
})



input element 테스트


React Component

아래와 같이 Add 버튼을 클릭하면, List에 추가하는 컴포넌트를 테스트한다.

import React, { useState } from 'react'
import "./AddInput.css"
import { v4 } from "uuid"
import TodoList from '../TodoList/TodoList'

function AddInput({
    setTodos, todos
}) {

    const [todo, setTodo] = useState("")

    const addTodo = () => {
        let updatedTodos = [
            ...todos,
            {
                id: v4(),
                task: todo,
                completed: false
            }
        ]
        setTodos(updatedTodos);
        setTodo("")
    }

    return (
        <div className="input-container">
            <input 
                className="input" 
                value={todo} 
                onChange={(e) => setTodo(e.target.value)}
                placeholder="Add a new task here..."
            />
            <button 
                className="add-btn"
                onClick={addTodo}
            >
                Add
            </button>
        </div>
    )
}

export default AddInput

테스트 코드 작성

0. describe로 test block으로 묶어 준다.

테스트는 3가지정도로 진행한다.

1. input 폼이 랜더링되는 지 확인한다.

2. input 폼에 change event로 입력한 값이 input value와 같은 지 테스트한다.

3. Add 버튼을 클릭시, input 폼이 clear되는 지 확인한다.

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

const mockedSetState = jest.fn();

describe("AddInput", () => {
  // #1 input 폼이 랜더링되는 지 확인한다.
  test("should render input element", async () => {
    render(<AddInput
      todos={[]}
      setTodos={mockedSetState} // setTodo는 function인데, 테스트에서는 사용하지 않으므로 jest.fn로 빈 함수를 만들어 준다.
    />)
    const inputEl = screen.getByPlaceholderText(/Add a new task here.../i)
    expect(inputEl).toBeInTheDocument();
  })

  // #2 input 폼에 change event로 입력한 값이 input value와 같은 지 테스트한다.
  test("should typable input element", async () => {
    render(<AddInput
      todos={[]}
      setTodos={mockedSetState}
    />)
    const inputEl = screen.getByPlaceholderText(/Add a new task here.../i)
    fireEvent.change(inputEl, { target: { value: 'hello world' } });
    expect(inputEl.value).toBe("hello world")
  })

  // #3 Add 버튼을 클릭시, input 폼이 clear되는 지 확인한다.
  test("should clear input element when add button is clicked", async () => {
    render(<AddInput
      todos={[]}
      setTodos={mockedSetState}
    />)
    const inputEl = screen.getByPlaceholderText(/Add a new task here.../i)
    const btnEl = screen.getByRole("button", { name: /Add/i })
    fireEvent.click(btnEl);
    expect(inputEl.value).toBe("")
  })
})

Ref

profile
Front-end Developer

0개의 댓글