React Testing Library 사전 학습

jybaek96·2022년 7월 24일
1

리액트 컴포넌트 테스트 라이브러리 종류

  • Jest
    • 자체적인 test runner와 test util 제공
  • React Testing Library
    • CRA에 내장 (Jest를 포함하는 구조)
    • 모든 테스트를 DOM 위주로 진행
    • 컴포넌트의 props나 state를 조회하는 일이 없음
    • 필요한 기능들만 지원 → 가볍고 일관성 있고 좋은 관습에 따르는 테스트 코드 작성 가능

1. 테스트 코드 작성 전 고려사항

  • 어떤 기능을 테스트할 것인지 명세서 작성
    • ex ) 렌더링시 모든 차트가 보이는지
    • 각 차트 개별 테스트시 제목, 차트가 렌더링되는지

2. React Testing Library 설치

  • CRA 프로젝트가 아니라면 Jest 설치
	npm i -D jest // 테스팅 라이브러리로 개발 의존성(-D 옵션)으로 설치
  • React Testing Library 및 Jest의 DOM에 특화된 matcher 애드온인 jest-dom 설치
npm i -D @testing-library/react 
// react-testing-library가 아닌 꼭 위 패키지 설치!
npm i -D @testing-library/jest-dom

3. React Testinh Library 설정

  • 각 테스트가 DOM에 렌더링해놓은 내용들을 테스트 종료시 다음 테스트를 위해 제거
  • jest-dom이 제공하는 matcher를 Jest 테스트 러너에게 인식

→ 아래 두줄 코드를 테스팅 설정 파일에 추가 (CRA 플젝이면 src/setupTest.js 파일 생성 후 추가)

import "@testing-library/react/cleanup-after-each";
import "@testing-library/jest-dom/extend-expect";

4. React Testing Library 주요 API

render()

  • DOM에 컴포넌트를 렌더링

  • @testing-library/react 모듈로부터 바로 임포트 가능하며, 인자로 렌더링할 React 컴포넌트를 넘김

  • React Testing Library가 제공하는 모든 쿼리 함수와 기타 유틸 함수를 담고 있는 객체 반환 → 해당 객체로부터 원하는 쿼리 함수만 얻어옴

    fireEvent()

  • 특정 이벤트를 발생시켜주는 객체

import { render, fireEvent } from "@testing-library/react";

const { getByText, getByLabelText, getByPlaceholderText } = render(
  <YourComponent />
);

5. 정적 컴포넌트 테스팅

내부 상태가 없고 단순히 고정된 텍스트와 이미지로 구성된 NotFound 컴포넌트

import React from "react";

function NotFound({ path }: { path: string }) {
  return (
    <>
      <h2>Page Not Found</h2>
      <p>해당 페이지({path})를 찾을 수 없습니다.</p>
      <img alt="404" src="https://media.giphy.com/media/14uQ3cOFteDaU/giphy.gif" />
    </>
  );
}

export default NotFound;
  • 특정 요소가 렌더링 되고 있는지 검증
    • getByText(str) - 특정 텍스트를 담고 있는 엘리먼트 반환
    • toBeInTheDocument() - 특정 요소가 화면에 존재하는지 검증
import React from "react";
import { render } from "@testing-library/react";
import NotFound from "./NotFound";

describe("<NotFound />", () => {
  it("renders header", () => {
    // render 함수의 인자로 테스트할 컴포넌트 전달 -> 리턴 객체로부터 getByText() 함수 얻음
    const { getByText } = render(<NotFound path="/abc" />);

    // 화면에서 검색할 텍스트인 'Page Not Found'를 함수의 인자로 넘긴 후 
    // 해당 텍스트를 담고 있는 <h2> 엘리먼트 획득
    const header = getByText("Page Not Found");

    // jest-dom의 toBeInTheDocument() matcher 함수 - 해당 <h2> 엘리먼트가 화면에 존재하는지 검증
    expect(header).toBeInTheDocument();
  });
});
  • 특정 요소 내부의 텍스트가 예상과 일치하는지 검증
    • toHaveTextContent(str) - 특정 요소 속의 텍스트가 예상과 일치하는지 검증
import React from "react";
import { render } from "@testing-library/react";
import NotFound from "./NotFound";

describe("<NotFound />", () => {
	... 중략
  // 특정 요소 내부의 텍스트가 예상과 일치하는지 검증
  it("renders paragraph", () => {
    // getByText 쿼리 함수는 정규식도 인자로 받을 수 있음
    const { getByText } = render(<NotFound path="/abc" />);
    const paragraph = getByText(/^해당 페이지/);

    // jest-dom의 toHaveTextContent matcher 함수 -  <p> 엘리먼트 속 텍스트가 예상과 일치하는지 검증
    expect(paragraph).toHaveTextContent("해당 페이지(/abc)를 찾을 수 없습니다");
  });
});
  • 이미지 검증
    • getAltText() - img 태그는 내부에 텍스트가 없기에 alt 속성값 이용
    • toHaveAttribute(속성, 값) - 특정 요소에 해당 속성과 그에 상응하는 속성값이 존재하는지 검증
import React from "react";
import { render } from "@testing-library/react";
import NotFound from "./NotFound";

describe("<NotFound />", () => {
  // 이미지 검증 (내부에 텍스트가 없기에 alt 속성값을 이용)
  it("renders image", () => {
    const { getByAltText } = render(<NotFound path="/abc" />);
    const image = getByAltText("404");

    // jest-dom의 toHaveAttribute() matcher 함수 - <img> 요소의 src 속성값이 정확한지 검증
    expect(image).toHaveAttribute("src", "https://media.giphy.com/media/14uQ3cOFteDaU/giphy.gif");
  });
})

6. 동적 컴포넌트 테스팅

내부 상태에 따라 UI 변화가 생길 수 있는 컴포넌트로 이메일, 비밀번호 입력란 & 버튼으로 구성된 로그인 폼

import React, { useState } from "react";

function LoginForm({ onSubmit }: { onSubmit: (e: React.FormEvent<HTMLFormElement>) => void }) {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");

  return (<>
    <h2>Login</h2>
    <form onSubmit={onSubmit}>
      <label>
        이메일
        <input type="email" placeholder="user@test.com" value={email} onChange={({ target: { value } }) => setEmail(value)} />
      </label>
      <label>
        비밀번호
        <input type="text" value={password} onChange={({ target: { value } }) => setPassword(value)} />
      </label>
      <button disabled={!email || !password}>로그인</button>
    </form>
  </>);
}

export default LoginForm
  • 비활성 되어 있던 버튼이 이메일, 비밀번호 입력시 활성화 되는지 검증
    • jest-dom의 toBeDisabled(), toBeEnabled() - 활성화 되어 있는지 검증
    • fireEvent.change - 2개의 입력칸에 change 이벤트 발생
import { fireEvent, render } from "@testing-library/react";
import React from "react";
import LoginForm from "./LoginForm";

describe("<LoginForm />", () => {
  it("enables button when both email and password are entered", () => {
    const { getByText, getByLabelText } = render(<LoginForm onSubmit={() => null} />);

    const button = getByText("로그인");
    const email = getByLabelText("이메일");
    const password = getByLabelText("비밀번호");

		// 로그인 버튼 비활성화 되어있는지 검증
    expect(button).toBeDisabled();

		// 입력값 수정
    fireEvent.change(email, { target: { value: "user@.test.com" } });
    fireEvent.change(password, { target: { value: "1234" } });
	
		// 입력 후 버튼 활성화 되어있는지 검증
    expect(button).toBeEnabled();
  });
});
  • 입력폼에 값 입력 후 로그인 버튼 클릭시 prop로 넘긴 onSubmit 함수 호출되는지 여부 검증
    • toHaveBeenCalledTimes(num) - 해당 요소가 예상한 수와 동일한 횟수로 호출되었는지 검증
    • jest.fn() 사용법
      • jest는 가짜 함수를 생성하며 jest.fn() 함수를 제공

      • 일반 자바스크립트 함수와 동일한 방식으로 인자를 넘겨 호출

        const mockFn = jest.fn();
        // 호출 결과 undefined
        mockFn();
        mockFn(1);
        mockFn("a");
        mockFn([1,2], {a: "b"});
        
        // 리턴값 설정
        mockFn.mockReturnValue("I am a mock");
        console.log(mockFn()); // I am a mock
        
        // 함수를 통째로 즉석에서 재구현
        mockFn.mockImplementation((name) => `I am ${name}`);
        console.log(mockFn("test")); // I am test
        
import { fireEvent, render } from "@testing-library/react";
import React from "react";
import LoginForm from "./LoginForm";

describe("<LoginForm />", () => {
  it("submits form when button is clicked", () => {
    const obSubmit = jest.fn(e => e.preventDefault());
    const { getByText, getByLabelText } = render(<LoginForm onSubmit={obSubmit} />);

    const button = getByText("로그인");
    const email = getByLabelText("이메일");
    const password = getByLabelText("비밀번호");

    fireEvent.change(email, { target: { value: "user@test.com" } });
    fireEvent.change(password, { target: { value: "1234" } });

    fireEvent.click(button);

    expect(obSubmit).toHaveBeenCalledTimes(1);
  });
})

7. 비동기 테스트

findBy로 시작하는 함수로 비동기 테스트를 수월하게 할 수 있다.

0개의 댓글