Unit Test
- 통합테스트에 비해 빠르고 쉬움
- 통합테스트를 진행하기 전에 문제를 찾아낼 수 있음(통합테스트 성공보장 X)
- 테스트 코드가 동작을 설명하는 명세가 됨(테스트를 읽고 어떤 동작을 하는지 예측 가능)
- 코드를 수정하거나 기능을 추가할 때 수시로 빠르게 검증할 수 있음
- 리팩토링 시에 안정성을 확보할 수 있음
- 개발 및 테스팅에 대한 시간과 비용을 절약할 수 있음
- TDD(Test-driven Development)
Facebook/jest
- React 의 영향이 크겠지만 가장 HOT 한 도구
- Easy Setup
- Instant Feedback
- 고친 파일만 빠르게 테스트 다시 해주는 기능 등..
- Snapshot Testing
- Component Test 에 중요한 역할을 하는 Snapshot
npm i jest -D
npm init -y
"scripts" : {
"test" : "jest"
}
// example.test.js
test('adds 1 + 2 th equal 3', () => {
expect(1 + 2).toBe(3);
});
npm test
// example.test.js
describe("expect test", () => {
it('37 equal 37', () => {
expect(37).toBe(3);
});
});
// example.test.js
describe("expect test", () => {
it('37 equal 37', () => {
expect(37).toBe(37);
});
it('{age: 39} to equal {age: 39}', () => {
expect({age: 39}).toEqual({age: 39});
});
});
npx jest —watchAll
(항상 테스트가 켜져있는 상태)
// example.test.js
describe("expect test", () => {
it('.toHaveLength', () => {
expect("Hello").toHaveLength(5); // 텍스트의 길이
});
});
// example.test.js
describe("expect test", () => {
it('.toHaveProperty', () => {
expect({name: "Mark"}).toHaveProperty('name');
expect({name: "Mark"}).toHaveProperty('name', "Mark");
}); // property를 가지고 있는지
it('.toBeDefined', () => {
expect({name: "Mark"}.name).toBeDefined();
}); // 정의 되어 있는지
it('.toBeFalsy', () => {
expect(false).toBeFalsy();
expect(0).toBeFalsy();
expect("").toBeFalsy();
expect(null).toBeFalsy();
expect(undefined).toBeFalsy();
expect(NaN).toBeFalsy();
}); // falsy 한 값인지
it('.toBeGreaterThan', () => {
expect(10).toBeGreaterThan(9);
}); // 값이 큰지
it('.toBeGreaterThanOrEqual', () => {
expect(10).toBeGreaterThanOrEqual(10);
}); // 값이 크거나 같은지
it('.toBeInstanceOf', () => {
class Foo {}
expect(new Foo()).toBeInstanceOf(Foo);
}); // instance 인지?
});
// .not.to~
describe('not.to~ test', () => {
it('.not.toBe', () => {
expect(37).not.toBe(36);
});
it('.not.toBeFalsy', () => {
expect(true).not.toBeFalsy();
expect(1).not.toBeFalsy();
expect('hello').not.toBeFalsy();
expect({}).not.toBeFalsy();
});
it('.not.toBeGreaterThan', () => {
expect(10).not.toBeGreaterThan(10);
});
});
// async test with done callback
// 비동기 방식
// 예전 방식
describe('use async test', () => {
it('setTimeout without done', () => {
setTimeout(()=>{
expect(37).toBe(36);
}, 1000);
});
it('setTimeout without done', done => {
setTimeout(()=>{
expect(37).toBe(36);
done();
}, 1000);
});
});
// async test with done promise
describe('use async test', () => {
it('promise then', () => {
function p(){
return new Promise(resolve => {
setTimeout(() => {
resolve(37);
}, 1000);
});
}
return p().then(data => expect(data).toBe(37));
});
it('promise catch', () => {
function p(){
return new Promise((resolve, rejects) => {
setTimeout(() => {
rejects(new Error('error'));
}, 1000);
});
}
return p().catch(e => expect(e).toBeInstanceOf(Error));
});
});
-----------------------------------------
describe('use async test', () => {
it('promise .resolves', () => {
function p(){
return new Promise(resolve => {
setTimeout(() => {
resolve(37);
}, 1000);
});
}
return expect(p()).resolves.toBe(37);
});
it('promise .rejects', () => {
function p(){
return new Promise((resolve, rejects) => {
setTimeout(() => {
rejects(new Error('error'));
}, 1000);
});
}
return expect(p()).rejects.toBeInstanceOf(Error);
});
});
// async test with async-await
// 가장 좋은 방식
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);
}
});
});
npx create-react-app react-component-test
npm test
a
Button 컴포넌트
- 컴포넌트 생성
- ‘button’ 이라고 쓰여있는 엘리먼트는 HTMLButtonElement 이다.
- 버튼을 클릭하면, p태그 안에 ‘버튼이 방금 눌렸다.” 라고 쓰여진다.
- 버튼을 클릭하기 전에는, p태그 안에 ‘버튼이 눌리지 않았다.” 라고 쓰여진다.
- 버튼을 클릭하고 5초 뒤에는, p태그 안에 ‘버튼이 눌리지 않았다.” 라고 쓰여진다.
- 버튼을 클릭하면, 5초동안 버튼이 비활성화 된다.
// src/components/Button.test.js
import { act, fireEvent, render } from "@testing-library/react";
import Button from './Button';
describe('Button 컴포넌트 (@testing-library/react)', () => {
it('컴포넌트가 정상적으로 생성된다.', () => {
const button = render(<Button />);
expect(button).not.toBe(null);
});
it('"button" 이라고 쓰여있는 엘리먼트는 HTMLButtonElement 이다.', () => {
const {getByText} = render(<Button />);
const buttonElement = getByText('button');
expect(buttonElement).toBeInstanceOf(HTMLButtonElement);
});
it('버튼을 클릭하면, p 태그 안에 "버튼이 방금 눌렸다." 라고 쓰여진다.', () => {
const {getByText} = render(<Button />);
const buttonElement = getByText('button');
fireEvent.click(buttonElement);
const p = getByText("버튼이 방금 눌렸다.");
expect(p).not.toBeNull();
expect(p).toBeInstanceOf(HTMLParagraphElement);
});
it('버튼을 클릭하기 전에는, p태그 안에 "버튼이 눌리지 않았다.” 라고 쓰여진다.', () => {
const {getByText} = render(<Button />);
const p = getByText("버튼이 눌리지 않았다.");
expect(p).not.toBeNull();
expect(p).toBeInstanceOf(HTMLParagraphElement);
});
it('버튼을 클릭하고 5초 뒤에는, p태그 안에 "버튼이 눌리지 않았다.” 라고 쓰여진다.', () => {
jest.useFakeTimers();
const {getByText} = render(<Button />);
const buttonElement = getByText('button');
fireEvent.click(buttonElement);
// 5초 흐른다.
act(()=>{
jest.advanceTimersByTime(5000);
});
const p = getByText("버튼이 눌리지 않았다.");
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).toBeDisabled();
// 5초 흐른다.
act(()=>{
jest.advanceTimersByTime(5000);
});
// 활성화
expect(buttonElement).toBeDisabled();
});
});
// src/components/Button.jsx
import { useRef, useState, useEffect } 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} 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);
}
}