React _ Testing

LOOPY·2021년 9월 3일
post-thumbnail

1. JavaScript Unit Test

  • Unit Test: 통합테스트에 비해 빠르고 쉬움, 통합테스트 전 문제 발견 가능, 테스트 코드의 동작을 설명하는 명세가 됨
    -> 🌟 선 코딩 후 몰아서 단위테스트 하지 말고, 단위테스트 작성 후 맞춰서 코딩하기 ; TDD(Test Driven Development)

2. Jest

  • Testing Frameork, 페이스북의 오픈소스, 가장 핫한 테스트 도구
  • 특징: Easy Setup, Instant Feedback, Snapshot testing
  • $ npm init -y $ npm i jest -D 이후 ./package.json의 script에 "test": "jest"
// ./example.test.js

// test("이름", () => { expect(실행코드).toBe(예상결과); });
test("adds 1 + 2 to equal 3",() => { 
  expect(1+2).toBe(3); 
});
// $ npm test
// PASS

// describe로 영역 구분, it와 test는 동일
describe("expect test", () => {
  it("37 to equal 37",()=>{ 
    expect(37).toBe(3); // FAIL
  });
  it("{age: 39} to equal {age: 39}", () => {
    expect({age: 39}).toBe({age: 39}); // FAIL(참조 값이 다르므로)
  });
  it("{age: 39} to equal {age: 39}",()=>{
    expect({age: 39}).toEqual({age: 39}); // PASS -> 객체 비교는 toEqual 사용하기
  });
  it(".toHaveLength", () => {
    expect("hello").toHaveLength(5); // 길이 구해 비교
  });
  it(".toHaveProperty", () => {
    expect({name: "Mark"}).toHaveProperty("name"); // 해당 속성을 가지고 있는지
    expect({name: "Mark"}).toHaveProperty("name", "Mark"); // 해당 속성과 값을 가지고 있는지
  });
  it(".toBeDefined", () => { // 해당 속성이 정의되어 있는지
    expect({name: "Mark"}.name).toBeDefined(); // PASS
  	expect({name: "Mark"}.age).toBeDefined(); // FAIL
  });
  it(".toBeFalsy", () => { // Falsy인지
    expect(false).toBeFalsy(); // PASS
    expect(0).toBeFalsy(); // PASS
    expect("").toBeFalsy(); // PASS
    expect(null).toBeFalsy(); // PASS
    expect(undefined).toBeFalsy(); // PASS
    expect(NAN).toBeFalsy(); // PASS
  });
  it(".toBeGreaterThan", () => {
    expect(10).toBeGreaterThan(9); // PASS
  });
  it(".toBeGreaterThanOrEqual", () => {
    expect(10).toBeGreaterThanOrEqual(10); // PASS
  });
  it(".toBeInstanceOf", () => {
    class foo{}
    expect(new Foo()).toBeInstanceOf(Foo); // PASS
  });
});
  • $ npx jest --watchAll
    -> 항상 테스트가 켜져있음 (코드 고치면 자동 테스트 수행)
  • 이외에 .toBeNull .toBeTruthy .toBeNaN
    -> 의도에 따라 앞에 .not 붙여 .not.toBe .not.toBeFalsy 가능

+) 비동기적 결과물 test (async test)

  • callback 사용, Promise 사용은 예전 방식
// 가장 좋은 방식: async 사용
describe("use async test", () => {
  it("async-await", async() => {
    function p(){
      return new Promise(resolve => {
        setTimeout(() => {
          resolve(37);
        }, 1000);
      });
    }

    const data = await p();
    return expect(data).toBe(37);
  });
});

// 또는

describe("use async test", () => {
  it("async-await, catch", async() => {
    function p(){
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          reject(new Error('error'));
        }, 1000);
      });
    }

    try {
      await p();
    } catch(error){
      expect(error).toBeInstanceOf(Error);
    }
  });
});

describe ("use async test", ())

3. React Component Test

  • $ npx create-react-app react-component-test
    -> create-react-app 으로 프로젝트 생성하면 이미 jest로 test 준비 끝낸 것 (따로 라이브러리 설치 필요 X)
  • test 파일은 이름.test.js로 만들거나 __tests__ 폴더 만들어 그 안에 넣기

4. testing-libraryreact 활용하기

  • Button 컴포넌트 만들기 연습
    1) button element는 HMTLButtonElement
    2) 버튼 클릭하면 p 태그 안에 CLICKED 메세지 출력
    3) 버튼 클릭 전 p 태그 안에 NORMAL 메세지 출력
    4) 버튼 클릭하고 5초 뒤면 클릭 전(NORMAL) 상태로 출력
    5) 버튼 클릭하면 5초 동안 비활성화
// ./src/components/Button.test.js
import {render} from "@testing-library/react";
import Button from "./Button"

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

describe('Button 컴포넌트 (@testing-library/react)', () => {
  it('컴포넌트 생성', () =>{
    const button = render(<Button />);
    expect(button).not.toBe(null);
  });
  it('button은 HTMLButtonElement', () =>{
    const {getByText} = render(<Button />);
    const HMTLButtonElement = getByText('button');
    expect(buttonElement).toBeInstanceOf(HTMLButtonElement);
  });
  it('버튼 클릭하면, p태그 안에 CLICKED 메세지 출력', () => {
    const {getByText} = render(<Button />);
    const buttonElement = getByText("button");
    fireEvent.click(buttonElement);
    const p = getByText(BUTTON_TEXT.CLICKED);

    expect(p).not.toBeNull();
    expect(p).toBeInstanceOf(HTMLParagraphElement);
  });
  it('버튼 클릭 전, p태그 안에 NORMAL 메세지 출력',  () => {
    const {getByText} = render(<Button />);
    const buttonElement = getByText("button");
    const p = getByText(BUTTON_TEXT.NORMAL);

    expect(p).not.toBeNull();
    expect(p).toBeInstanceOf(HTMLParagraphElement);
  });
  it('버튼 클릭 후 5초 뒤, p태그 안에 다시 NORMAL 메세지 출력',  () => {
    jest.useFakeTimers();

    const {getByText} = render(<Button />);
    const buttonElement = getByText("button");
    fireEvent.click(buttonElement);

    act(() => { // test code에서
      jest.advanceTimersByTime(5000); // 5초 지난 시점 임의로 설정
    });

    const p = getByText(BUTTON_TEXT.NORMAL);
    expect(p).not.toBeNull();
    expect(p).toBeInstanceOf(HTMLParagraphElement);
  });
  it('버튼 클릭하면, 5초 동안 버튼 비활성화',  () => {
    jest.useFakeTimers();

    const {getByText} = render(<Button />);
    const buttonElement = getByText("button");
    fireEvent.click(buttonElement);

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

    act(() => {
      jest.advanceTimersByTime(5000); // 5초 지난 시점 임의로 설정
    });

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

// ./src/components/Button.jsx
const BUTTON_TEXT = {
  NORMAL: '버튼이 눌리지 않았다.',
  CLICKED: '버튼이 방금 눌렸다.'
}

export default function Button(){

  const[message, setMessage] = useState(BUTTON_TEXT.NORMAL);
  const timer = useRef();

  useEffect(() => {
    return () => {
      // ComponentWillUnmount 시점에 timer 살아있다면 clear 해줘야 함
      if(timer.current){
        clearTimeout(timer.curent); 
      }
    };
  }, []);

  return(
    <div>
      <button onClick={click} disabled={message === BUTTON_TEXT.CLICKED}>
    	button
      </button>
      <p>{message}</p>
    </div>
  );

  function click(){
    setMessage(BUTTON_TEXT.CLICKED);
    // 길 잃는 경우 방지 위해 timer.current에 저장
    timer.current = setTimeout(()=>{ 
      setMessage(BUTTON_TEXT.NORMAL);
    }, 5000);
  }
}
profile
2년차 프론트엔드 개발자의 소소한 기록을 담습니다 :-)

0개의 댓글