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
- promise를 return 하는 함수를 만들어서 해당 함수가 실행되어 promise가 만들어지면
- rejcet test
- promise를 return하는 함수를 만들어서 해당 함수가 실행되어 promise가 만들어지면
catch
를 통해서 reject(error)를 받아서 expect에 넣어 Error test 함
- promise를 return하는 함수를 만들어서 해당 함수가 실행되어 promise가 만들어지면
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 해야 함
useEffec
t의 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 말고)