HooneyLog

react-testing-library를 사용하여 간단한 테스트 코드 작성해보기

프로필 이미지

Seunghoon Shin

2022년 4월 27일 24:53

안녕하세요! 이번시간에는 react-testing-library를 사용하여 간단하게 몇가지 코드를 살펴보며 react에서는 테스트코드를 어떻게 짜는지를 알아보도록 하겠습니다!!

우선 테스트 코드를 잘 모르시는 분들을 위해 말씀드리면

테스트 코드란 쉽게말해서 우리가 개발한 코드들이 우리가 원하는 기대값으로 정상작동 하는지를 테스트 하는 코드를 짜는 것입니다!!

왜 테스트코드를 짜야할까?

개발하다보면 어쩔 수 없이 많은 legacy 코드들이 탄생하게 되고 반복적인 리팩토링 작업이 필요할때가 많습니다!

하지만 테스트 코드들이 짜여지 않은 상태에서 코드들을 수정하고 만지는 작업을 하게 되면 여러 컴포넌트들과 기능들이 의존관계가 있어 생각하지 못한 곳에서 side effect들이 발생하곤 하죠!

만약 우리가 테스트코드를 미리 잘 짜놓았고, 단위 및 통합테스트 코드를 잘 짜놓았다면 잘못된 결과값을 도출하는 코드를 짜놓았을때 개발자가 바로 현재 코드가 잘못되었다고 fail이라는 소식을 알려주게 됩니다!

때문에 개발자들은 이것을 인지하고 다른 방식으로 다시 개발을 진행하게 되겠죠. 이렇기때문에 테스트 코드는 지속적인 유지보수가 필요한 프로젝트에서는 매우 중요한 것입니다!! 개발자들에게 자신감을 높여줄 뿐더러 더욱더 깔끔한 코드도 만들 수 있게 해주죠!

그럼 바로 테스트 코드의 예시를 살펴보도록 하겠습니다

  • 컴포넌트가 잘 렌더링이 되는지
  • 앵커태그의 href가 적상적으로 작동하는지 (우리가 원하는 페이지로 제대로 이동이 가는지 테스트)
  • 버튼 클릭했을때 handleClick 함수가 정상 작동하는지 테스트
  • Api 서버 Mocking 후 반환 state값이 잘 렌더링 되는지 테스트
  • 간단한 커스텀 훅 테스트
  • 컴포넌트가 잘 렌더링이 되는지

  • Person.test.jsx
  • import { render, screen } from "@testing-library/react"
    import Person from "./Person"
    
    test('내 이름이 들어간 컴포넌트가 잘 들어갔는지 테스트',()=>{
        render(<Person name="신승훈"/>)
        const element = screen.getByText(/제 이름은 신승훈입니다!/i);
        expect(element).toBeInTheDocument();
    })
  • Person.jsx
  • import React from 'react';
    
    const Person = ({name}) => {
        return (
            <div>
                안녕하세요! 제 이름은 {name}입니다!
            </div>
        );
    };
    
    export default Person;
  • Person 이라는 컴포넌트를 렌더링 시킨다
  • 렌더링된 컴포넌트에 screen이라는 메서드를 통해 탐색할 수 있고, “제 이름은 신승훈입니다” 라는 텍스트가 있는 요소를 element 의 변수에 할당한다
  • 그 element가 document안에 있는지를 체크한다
  • 앵커태그의 href가 적상적으로 작동하는지 (우리가 원하는 페이지로 제대로 이동이 가는지 테스트)

  • Link.test.jsx
  • import { render, screen } from "@testing-library/react"
    import Link from "./Link"
    
    test('앵커 element에 text가 적상 작동하고 링크 이동이 정상 작동하는지 테스트',()=>{
        const items = [{
            text:"후니블로그로 가보자!",
            href:"https://www.hooneylog.com"
        }]
        render(<Link items={items}/>);
        const elements = screen.getAllByRole("link");
        expect(elements[0]).toHaveTextContent(items[0].text);
        expect(elements[0]).toHaveAttribute("href",items[0].href);
    })
  • Link.jsx
  • import React from 'react';
    
    const Link = ({items}) => {
        return (
            <>
                {
                    items.map(({text,href})=><a href={href}>{text}</a>)
                }
            </>
        );
    };
    
    export default Link;
  • a tag에 원하는 props를 넘긴다
  • a tag가 가지고 있는 role를 이용해서 element를 받아온다 (getAll를 사용하여 해당 하는 모든 요소를 배열로 받아온다) → 만약 role 에 대해서 모른다면?
  • 해당 태그에 원하는 텍스트와 속성값이 있는지 체크한다.
  • 버튼 클릭했을때 handleClick 함수가 정상 작동하는지 테스트

  • Button.test.jsx
  • /* eslint-disable testing-library/no-render-in-setup */
    import { fireEvent, render, screen } from "@testing-library/react"
    import Button from "./Button"
    
    beforeEach(()=>{
        render(<Button/>);
    })
    
    describe('<Button/>',()=>{
        test('초기 counte가 0으로 잘 렌더링이 되는지 테스트',()=>{
            const element = screen.getByRole("countInfo");
            expect(element).toHaveTextContent("Current Number is 0");
        })
    
        test('버튼을 클릭했을때 alert가 잘 동작하는지 테스트',async()=>{
            const alertMock = jest.spyOn(window,"alert").mockImplementation();
    
            const buttonElement = screen.getByRole("button",{
                name:"카운트 올라가라!"
            });
    
            fireEvent.click(buttonElement);
            expect(alertMock).toHaveBeenCalledTimes(1);
    
    
        })
        test('버튼을 클릭했을때 카운트가 잘 올라가는지 테스트',async()=>{
            const buttonElement = screen.getByRole("button",{
                name:"카운트 올라가라!"
            });
    
            fireEvent.click(buttonElement);
    
            const divElement = screen.getByRole("countInfo");
            expect(divElement).toHaveTextContent("Current Number is 1");
    
            fireEvent.click(buttonElement);
            expect(divElement).toHaveTextContent("Current Number is 2");
        })
    })
  • Button.jsx
  • import React from 'react';
    import { useState } from 'react';
    
    const Button = () => {
        const [count,setCount] = useState(0);
    
        const handleClick = () => {
            alert("등장!")
            setCount(count + 1);
        }
        return (
            <>
                <div role="countInfo">Current Number is {count}</div>
                <button onClick={handleClick}>카운트 올라가라!</button>
            </>
        );
    };
    
    export default Button;
  • 각 테스트 마다 <Button/> 컴포넌트를 렌더링하기위해 beforeEach 사용
  • div에 임의로 준 role를 이용하여 getByRole을 사용하여 해당 element 가져오기
  • 초기 div에 count state가 0인 상태로 렌더링이 잘되는지 테스트
  • jest.spyon을 활용하여 alert 메서드 가져오기 → spyon이란? // jest.spyOn(object,”methodname”)
  • 버튼 클릭했을때 alert 메서드가 1번 잘 호출되었는지 확인
  • 버튼 클릭했을때 count state 가 적상적으로 1씩 올라가는지 확인
  • Api 서버 Mocking 후 반환 state값이 잘 렌더링 되는지 테스트

  • Api.test.jsx
  • import { render, screen } from '@testing-library/react'
    import {rest} from 'msw'
    import {setupServer} from 'msw/node'
    import Api from './Api'
    
    
    const server = setupServer(
        rest.get('/user', (req, res, ctx) => {
          return res(ctx.json({name: '신승훈'}))
        }),
      )
    
      beforeAll(() => server.listen())
      afterEach(() => server.resetHandlers())
      afterAll(() => server.close())
    
      test('api mocking 후 state 값이 잘 렌더링 되는지',async ()=>{
          render(<Api/>);
          const element = await screen.findByRole("contentInfo");
          expect(element).toHaveTextContent("안녕하세요, 제 이름은 신승훈입니다.")
  • Api.jsx
  • import React from 'react';
    import { useState } from 'react';
    import { useEffect } from 'react';
    
    const Api = () => {
        const [name,setName] = useState(null);
        useEffect(()=>{
            const dispatchFetch = async () => {
                const response = await fetch("/user");
                const result = await response.json();
                setName(result.name)
            }
    
            dispatchFetch();
        },[])
        return (
            <div>
                {
                    name && <div role="contentInfo">
                    안녕하세요, 제 이름은 {name}입니다.
                </div>
                }
    
            </div>
        );
    };
    
    export default Api;
  • msw 라이브러리를 다운받아 mock server를 위한 셋팅
  • 엔드 포인트를 만들어 원하는 결과값을 넣어 mocking을 한다
  • findByRole를 사용하여 해당 엘리먼트가 등장할때까지 동기처리를 하고 원하는 결과값이 잘 렌더링 되는지 테스트
  • 간단한 커스텀 훅 테스트

  • useCounter.test.js
  • import { renderHook, act } from "@testing-library/react-hooks";
    import useCounter from "./useCounter";
    
    test("increment가 적상작동 하는지 테스ㅡㅌ", () => {
      const { result } = renderHook(() => useCounter());
    
      act(() => {
        result.current.increment();
      });
    
      expect(result.current.count).toBe(1);
    });
  • useCounter.js
  • import { useState, useCallback } from "react";
    
    export default function useCounter() {
      const [count, setCount] = useState(0);
      const increment = useCallback(() => setCount((x) => x + 1), []);
      return { count, increment };
    }
  • useCounter훅을 react-testing-library에서 제공해주는 rederHook 메서드를 이용하여 커스텀훅을 렌더링
  • hook 안에 선언한 increment 메서드를 act 메서드를 사용하여 실행
  • 그리고 해당 기대값이 충족하는지 테스트
  • 이상으로 간단한 리액트 테스트 코드들을 살펴봤습니다!

    감사합니다