20210611 React11 : React Component Test, Testing-library/react, component testing 예제, Refactoring(Function Clear), jest-dom, user-event

5 분 소요

React 11



React Component Test


  • 기본적으로 개발을 할때 testing 코드를 먼저 작성하고 개발 코드를 작성


Testing-library/react


  • 기존의 jest testing 함수 말고, 더 다양하게 testing을 하기 위한 함수를 제공하는 라이브러리
  • CRA 프로젝트인 경우 자체적으로 설치 되어 있음
  • @testing-library/react



testing 작성 예시

  • 구현할 요소와 기능
    • 컴포넌트가 정상적으로 생성된다.
    • “button” 이라고 쓰여진 엘리먼트는 HTMLButtonElement 이다.
    • 버튼을 클릭하면, p 태그 안에 “버튼이 방금 눌렸다”라고 쓰여진다.
    • 버튼을 클릭하기 전에는, p 태그 안에 “버튼이 눌리지 않았다”라고 쓰여진다.
    • 버튼 클릭 5초 후에는, p 태그 안에 “버튼이 눌리지 않았다”라고 쓰여진다.
    • 버튼을 클릭하면, 5초 동안 버튼이 비활성화 된다.



TEST code 작성


1. 컴포넌트가 정상적으로 생성된다.


  • render() : testing-library/react에서 제공하는 함수로, 컴포넌트가 쓰여지는 경우 제대로 표시 되는지 test 하는 코드로 해당 컴포넌트 표현을 넣고 변수로 받아서 null인지 test
import { render } from "@testing-library/react";
import Button from "./Button";

describe("Button 컴포넌트 (@testing-library/react)", () => {
  it("컴포넌트가 정상적으로 생성된다.", () => {
    const button = render(<Button />);
    expect(button).not.toBe(null);
  });
});
// Button.jsx
export default function Button() {
  return <></>;
}




2. “button” 이라고 쓰여진 엘리먼트는 HTMLButtonElement 이다.


  • testing-library가 제공하는 render함수에서 getByText라는 함수를 통해서 render안의 해당 컴포넌트를 구성하고 있는 요소들중 getByText 인자로 들어오는 string으로 내부를 구성하는 element를 가져옴
  • react에서 제공하는 Element 중 Button 객체와 일치하는지 확인 (HTMLButtonElement)
import { render } from "@testing-library/react";
import Button from "./Button";

describe("Button 컴포넌트 (@testing-library/react)", () => {
  it('"button" 이라고 쓰여진 엘리먼트는 HTMLButtonElement 이다.', () => {
    const { getByText } = render(<Button />);
    const buttonElement = getByText("button");
    // button이라는 text가 들어있는 element를 구해옴

    expect(buttonElement).toBeInstanceOf(HTMLButtonElement);
  });
});
// Button.jsx
export default function Button() {
  return <button>button</button>;
}




3. 버튼을 클릭하면, p 태그 안에 “버튼이 방금 눌렸다.”라고 쓰여진다.


  • 해당 버튼을 가져오고 fireEvent 객체를 통해서 특정 event 종류 함수의 인자로 해당 버튼 요소를 넣으면 해당 이벤트가 반응
  • 눌린 후 ‘버튼이 방금 눌렸다’ 라는 내용의 element를 가져와서 해당 element가 있는지 null test
  • 그리고 그게 p태그 요소인지 test
import { render, fireEvent } from "@testing-library/react";
import Button from "./Button";

describe("Button 컴포넌트 (@testing-library/react)", () => {
  it('버튼을 클릭하면, p 태그 안에 "버튼이 방금 눌렸다."라고 쓰여진다.', () => {
    const { getByText } = render(<Button />);
    const buttonElement = getByText("button");

    fireEvent.click(buttonElement);
    const p = getByText("버튼이 방금 눌렸다.");
    expect(p).not.toBeNull();
    expect(p).toBeInstanceOf(HTMLParagraphElement);
  });
});
// Button.jsx
export default function Button() {
  return (
    <div>
      <button>button</button>
      <p>버튼이 방금 눌렸다.</p>
    </div>
  );
}




4. 버튼을 클릭하기 전에는, p 태그 안에 “버튼이 눌리지 않았다.”라고 쓰여진다.


  • 똑같이 위와 같이 하되, click event 없이 test 코드 작성
import { render, fireEvent } from "@testing-library/react";
import Button from "./Button";

describe("Button 컴포넌트 (@testing-library/react)", () => {
  it('버튼을 클릭하기 전에는, p 태그 안에 "버튼이 눌리지 않았다."라고 쓰여진다.', () => {
    const { getByText } = render(<Button />);

    const p = getByText("버튼이 눌리지 않았다.");
    expect(p).not.toBeNull();
    expect(p).toBeInstanceOf(HTMLParagraphElement);
  });
});
// Button.jsx
import { useState } from "react";

export default function Button() {
  const [message, setMessage] = useState("버튼이 눌리지 않았다.");
  return (
    <div>
      <button onClick={click}>button</button>
      <p>{message}</p>
    </div>
  );
  function click() {
    setMessage("버튼이 방금 눌렸다.");
  }
}




5. 버튼 클릭 5초 후에는, p 태그 안에 “버튼이 눌리지 않았다”라고 쓰여진다.


  • 5초 시간의 경과를 만들어 test 해야함
  • jest.useFakeTimers() : 진짜 시간의 경과를 기다리기 보다는, 프로그램의 시간만 돌려서 test 함
    • jest.advanceTimersByTime(시간)
  • AAA (Arrange Act Assert, test 방법론 중의 하나 임) : state가 변화가 있는 test를 하는 경우 act()라는 함수를 사용해서 해당 함수를 넣고 state가 변화하는 행위가 지나가도록 해야함
    • 여기서 시간이 흐르는 것도 state가 변하기 때문에 act를 사용함
import { act, render, fireEvent } from "@testing-library/react";
import Button from "./Button";

describe("Button 컴포넌트 (@testing-library/react)", () => {
  it('버튼 클릭 5초 후에는, p 태그 안에 "버튼이 눌리지 않았다"라고 쓰여진다.', () => {
    jest.useFakeTimers();
    const { getByText } = render(<Button />);
    const buttonElement = getByText("button");

    fireEvent.click(buttonElement);

    // 5초 흐른다.
    // 시간이 흐르거나, 버튼을 클릭하거나, state가 변하거나 모두 act를 사용해야 함 (AAA 패턴, Arrange Act Assert)
    act(() => {
      jest.advanceTimersByTime(5000);
    });

    const p = getByText("버튼이 눌리지 않았다.");
    expect(p).not.toBeNull();
    expect(p).toBeInstanceOf(HTMLParagraphElement);
  });
});




Refactoring (리팩토링) : Function Clear, 값-변수 관리


  • Handler 제거 해주기 (clear)
    • setTimeout이 돌고 있는 중에(즉, 함수가 처리하고 있는 중에), 요소가 unmount 되어 없어지는 경우에도 계속 함수를 처리할 수 있는 일이 벌어질 수 있기 때문에 해당 함수를 clear 해야 함
    • useEffect의 return 함수 로 해당 handler를 clear 함
    • setTimeout() 메서드는 일정 시간(밀리초 단위)이 흐른 후에 실행할 함수를 지정한다. setTimeout()의 반환값clearTimeout() 메서드의 인자로 사용하면 계획된 함수의 실행을 취소 가능함
    • 반환값은 useRef를 통해서 참조할 수 있도록 함
  • 표시하는 string 내용 자체를 넣기 보단, TEXT 값으로 객체형태로 변수로 보관하여 사용함
import { useEffect, useRef, useState } from "react";

const BUTTON_TEXT = {
  NORMAL: "버튼이 눌리지 않았다.",
  CLICKED: "버튼이 방금 눌렸다.",
};

export default function Button() {
  const [message, setMessage] = useState(BUTTON_TEXT.NORMAL);
  const timer = useRef();

  useEffect(() => {
    return () => {
      if (timer.current) {
        clearTimeout(timer.current);
      }
    };
  }, []);
  return (
    <div>
      <button onClick={click}>button</button>
      <p>{message}</p>
    </div>
  );
  function click() {
    setMessage(BUTTON_TEXT.CLICKED);
    timer.current = setTimeout(() => {
      setMessage(BUTTON_TEXT.NORMAL);
    }, 5000);
  }
}




6. 버튼을 클릭하면, 5초 동안 버튼이 비활성화 된다.


  • disabled 속성을 제어하면 됨
  • 클릭해서 시간이 지난후, 전의 disabled의 true false를 체크함
import { act, render, fireEvent } from "@testing-library/react";
import Button from "./Button";

describe("Button 컴포넌트 (@testing-library/react)", () => {
  it("버튼을 클릭하면, 5초 동안 버튼이 비활성화 된다.", () => {
    jest.useFakeTimers();
    const { getByText } = render(<Button />);
    const buttonElement = getByText("button");

    fireEvent.click(buttonElement);

    // 비활성화
    expect(buttonElement.disabled).toBeTruthy();

    act(() => {
      jest.advanceTimersByTime(5000);
    });

    // 활성화
    expect(buttonElement.disabled).toBeFalsy();
  });
});
import { useEffect, useRef, useState } from "react";

const BUTTON_TEXT = {
  NORMAL: "버튼이 눌리지 않았다.",
  CLICKED: "버튼이 방금 눌렸다.",
};

export default function Button() {
  const [message, setMessage] = useState(BUTTON_TEXT.NORMAL);
  const timer = useRef();

  useEffect(() => {
    return () => {
      if (timer.current) {
        clearTimeout(timer.current);
      }
    };
  }, []);
  return (
    <div>
      {/*disabled 속성을 text로 제어함*/}
      <button onClick={click} disabled={message === BUTTON_TEXT.CLICKED}>
        button
      </button>
      <p>{message}</p>
    </div>
  );
  function click() {
    setMessage(BUTTON_TEXT.CLICKED);
    timer.current = setTimeout(() => {
      setMessage(BUTTON_TEXT.NORMAL);
    }, 5000);
  }
}




testing-library 그 외


jest dom

  • gitHub : testing-library/jest-dom
  • 여러 상황의 testing 코드를 조금 더 보기 쉽게 testing 함수로 구현 해놓음으로 써 testing 코드에 사용할 수 있게 해줌
it("버튼을 클릭하면, 5초 동안 버튼이 비활성화 된다.", () => {
  jest.useFakeTimers();
  const { getByText } = render(<Button />);
  const buttonElement = getByText("button");

  fireEvent.click(buttonElement);

  // 비활성화
  expect(buttonElement.disabled).toBeDisabled();

  act(() => {
    jest.advanceTimersByTime(5000);
  });

  // 활성화
  expect(buttonElement.disabled).not.toBeDisabled();
});


user-event

태그:

업데이트: