20210611 React11 : React Component Test, Testing-library/react, component testing 예제, Refactoring(Function Clear), jest-dom, user-event
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
- testing-library/user-event
 - user가 event를 날릴때 그것을 test 할 수 있음 (fireEvent 말고)