본문 바로가기

React

20210611 React10 : JavaScript Unit Test, JEST, TEST 파일 작성, TEST 코드 함수(to, 비동기)

React 10



React Testing

JavaScript Unit Test

  • 통합테스트에 비해 빠르고 쉽다.
  • 통합테스트를 진행하기 전에 문제를 찾아 낼 수 있다. (단, 통합테스트 성공 보장은 없음)
  • 테스트 코드가 살아있는(동작을 설명하는) 명세가 됨
    • 테스트를 읽고 어떻게 동작하는지도 예측 가능
  • 선 코딩 후, 몰아서 단위테스트가 아니라 TDD를 해야함
  • 단위테스트를 작성하고, 그것에 맞는 코딩을 작성하는 버릇이 필요 함



Jest

  • Javascript testing FrameWork로 CRA에서 기본적으로 제공하는 test 로서 입지를 다져 대중적으로 쓰이고 있음
  • facebook의 openSource로 가장 핫한 테스트 도구
  • Easy Setup, Instant Feedback(고친 파일만 빠르게 테스트 다시 해주는 기능 등)
  • Snapshot Testing (컴포넌트 테스트에 중요한 역할을 하는 스냅샷)
  • CRA로 project를 만들면 jest가 이미 설정되어 있어 설치할 필요 없음

  • 설치 : npm i jest -D
  • package.json script test 변경하기 "test": "jest"
  • test 파일 작성하기 (example.test.js)



Test code 파일 작성구조


describe(".to ~ test", () => {
  test("adds 1 + 2 to equal 3", () => {
    expect(1 + 2).toBe(3);
  });
});
  • describe() 함수는 test code 들을 묶어 내는 한 덩어리 설명임
  • test() 함수는 it()함수로 대체 되어 사용 가능함
  • test 진행 : npm test
  • 코드가 입력되면서 자동으로 계속 testing을 하게 하는 명령어
    • npx jest --watchAll
    • 항상 tester가 켜져있는 상태로 코드가 수정되고 저장되면 그것을 test함



test code에서 사용되는 비교 함수


to 관련 test 함수


  • toBe, toEqual, toHaveLength, toHaveProperty, toBeDefined, toBeFalsy, toBeGreaterThan, toBeGreaterThanOrEqual, toBeInstanceOf, toBeNull, toBeTruthy, toBeUndefined, not
describe(".to ~ test", () => {
  it(".toBe", () => {
    expect(37).toBe(37);
    // toBe() -> reference까지 고려한 비교
  });

  it(".toEqual", () => {
    expect({ age: 39 }).toEqual({ age: 39 });
    // toEqual() -> reference 위치 고려하지 않고 생긴것만 고려
  });

  it(".toHaveLength", () => {
    expect("hello").toHaveLength(5);
    // toHaveLength() -> 길이 test
  });

  it(".toHaveProperty", () => {
    expect({ name: "kim" }).toHaveProperty("name");
    // property test
    expect({ name: "kim" }).toHaveProperty("name", "kim");
    // property와 value까지 test
  });

  it(".toBeDefined", () => {
    expect({ name: "kim" }.name).toBeDefined();
    // property 에 name이 있는지
    // expect({ name: 'kim' }.age).toBeDefined();
    // property에 age가 있는지
  });

  it(".toBeFalsy", () => {
    // Falsy 한 값인지 test
    expect(false).toBeFalsy();
    expect(0).toBeFalsy();
    expect("").toBeFalsy();
    expect(null).toBeFalsy();
    expect(undefined).toBeFalsy();
    expect(NaN).toBeFalsy();
  });

  it(".toBeGreaterThan", () => {
    // 기대값이 더 큰지 test
    expect(10).toBeGreaterThan(9);
  });

  it(".toBeGreaterThanOrEqual", () => {
    // 기대값이 더 크거나 같은지 test
    expect(10).toBeGreaterThanOrEqual(10);
  });

  it(".toBeInstanceOf", () => {
    // 기대값인 instance가 해당 클래스에 속하는지 test
    class Foo {}
    expect(new Foo()).toBeInstanceOf(Foo);
  });

  it(".toBeNull", () => {
    // 기대값이 null인지 test
    expect(null).toBeNull();
  });

  it(".toBeTruthy", () => {
    // 기대값이 Truthy 한지
    expect(true).toBeTruthy();
    expect(1).toBeTruthy();
    expect("hello").toBeTruthy();
    expect({}).toBeTruthy();
  });

  it(".toBeUndefined", () => {
    // 기대값이 Undefined 한지
    expect(NaN).toBeNaN();
  });
});

// not을 붙여 비교 함수를 반대로 비교 해서 사용 할 수 있음
describe(".not.to ~ test", () => {
  it(".not.toBe", () => {
    expect(37).not.toBe(36);
  });
  it(".not.toBeFalsy", () => {
    expect(1).not.toBeFalsy();
    expect(true).not.toBeFalsy();
    expect("hello").not.toBeFalsy();
    expect({}).not.toBeFalsy();
  });
  it(".not.toBeGreaterThan", () => {
    expect(32).not.toBeGreaterThan(36);
  });
});



비동기 관련 test 함수


  • 비동기 관련해서 test하는 방식은 3가지가 있음
    • Callback, Promise, await-async

Callback 방식


  • done() 함수를 인자로 받아서 done()이 호출될때 까지 code test를 기다려줌 -> 늦게 호출되는 부분 test 가능
describe("use async test with callback", () => {
  it("setTimeout without done", () => {
    // done이 없는 경우 비동기 함수가 실행되고 끝나버림
    setTimeout(() => {
      expect(37).toBe(37);
    }, 1000);
  });
  it("setTimeout with done", (done) => {
    // done 함수를 받아 사용하는 경우 done 함수가 실행될 때 까지 비동기 함수부분이 끝나지 않음
    setTimeout(() => {
      expect(37).toBe(37); // 해당 코드 test됨
      done();
    }, 1000);
  });
});



promise 방식


  • resolve, reject를 구분해서 test를 함

then, catch 방식


  • resolve test
    • promise를 return 하는 함수를 만들어서 해당 함수가 실행되어 promise가 만들어지면 then을 통해 callback으로 resolve를 받아 해당 resolve를 expect에 넣어 test
  • rejcet test
    • promise를 return하는 함수를 만들어서 해당 함수가 실행되어 promise가 만들어지면 catch를 통해서 reject(error)를 받아서 expect에 넣어 Error test 함
describe('use async test with promise', () => {
    it('promise then, resolve test', () => {
        function p() {
            return new Promise((resolve) => {
                setTimeout(() => {
                    resolve(37);
                }, 1000);
            });
        }
        // return을 해줘야 비동기 함수가 끝나기 전에 expect를 체크함
        return p().then((data) => expect(data).toBe(37));
    });
    it('promise catch, reject test', () => {
        function p() {
            return new Promise((resolve, reject) => {
                setTimeout(() => {
                    reject(new Error('error'));
                }, 1000);
            });
        }
        // return을 해줘야 비동기 함수가 끝나기 전에 expect를 체크함
        return p().catch((e) => expect(e).toBeInstanceOf(Error));
    });



resolves, rejects 방식 (then, catch 없는 방식)


  • resolve test : promise를 return하는 함수를 expect에 넣고, 실행되면 resovles라는 속성으로 resolve를 받아서 바로 비교함
  • reject test : promise를 return하는 함수를 expect에 넣고, 실행되면 rejects라는 속성응로 reject를 받아서 바로 비교함

    // then, catch 없이 바로 expect에 넣어서 검사하는 방식
    it('promise .resolves test', () => {
        function p() {
            return new Promise((resolve) => {
                setTimeout(() => {
                    resolve(37);
                }, 1000);
            });
        }
        // return을 해줘야 비동기 함수가 끝나기 전에 expect를 체크함
        return expect(p()).resolves.toBe(37);
    });
    it('promise .reject test', () => {
        function p() {
            return new Promise((resolve, reject) => {
                setTimeout(() => {
                    reject(new Error('error'));
                }, 1000);
            });
        }
        // return을 해줘야 비동기 함수가 끝나기 전에 expect를 체크함
        return expect(p()).rejects.toBeInstanceOf(Error);
    });
});



async, await 키워드 방식


  • resolve test : promise를 return하는 함수를 await로 실행 시켜 resolve를 다른 변수로 받아오고, 해당 변수를 expect에 넣어 비교 test
  • reject test : promise를 return하는 함수를 await로 실행시켜 try 문에 넣어 받고, catch 문을 붙여 reject(error)를 받아서 expect에 넣어 비교함
describe("async test with async-await keywords", () => {
  it("async-await, resolve test", async () => {
    function p() {
      return new Promise((resolve) => {
        setTimeout(() => {
          resolve(37);
        }, 1000);
      });
    }
    const data = await p();
    return expect(data).toBe(37);
  });

  it("async-await, catch reject test", async () => {
    function p() {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          reject(new Error("error"));
        }, 1000);
      });
    }
    try {
      await p();
    } catch (error) {
      expect(error).toBeInstanceOf(Error);
    }
  });
});



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