현재 가장 핫한 테스트 도구
Easy Setup
Instatnt Feddback ( 고친 파일만 빠르게 테스트 다시 해주는 기능 등.)
Snapshot Testing ( 컴포넌트 테스트에 중요한 역할을 하는 스냅샷 )
CRA 에는 jest 가 내장되어있다.
npm init -y 하여 테스트해보기
npm i jest
"script" : {
"test" : "jest"
}
test("adds 1+2 to equal 3", ()=>{
expect(1+2).toBe(3);
})
npm test
describe("expect test", ()=>{
it("37 to equal 37", ()={
expect(37).toBe(37);
});
it("{age:39} to equal {age:39}", ()={
expect({age:39}).toEqual({age:39});
});
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();
expect({name: "Mark"}.age).toBeDefined(); //Error
});
it(".toBeFalsy", ()={
expect(false).toBeFalsy();
expect(0).toBeFalsy();
expect('').toBeFalsy();
expect(null).toBeFalsy();
expect(undefined).toBeFalsy();
expect(NaN).toBeFalsy();
});
it(".toBeGreaterThan", ()={
expect(10).toBeGreaterThan(9);
});
it(".toBeGreaterThanOrEqual", ()={
expect(10).toBeGreaterThanOrEqual(10);
});
it(".toBeInstanceOf", ()={
class Foo {}
expect(new Foo()).toBeInstanceOf(Foo);
});
});
원시값이 아닌 객체를 비교하기 위해서는 toBe가 아닌 toEqual 을 사용해야 한다.
.not.to 로 반대경우를 테스트 할 수 있다.
테스트 계속 켜놓기
npx jest --watchAll
describe('use async test', ()=>{
it('setTimeout without done', (done)=>{
setTimeout(()=>{
expect(37).toBe(36);
done();
},1000)
})
})
it 호출 함수에 인자로 done 을 넣고 setTimeout 실행 함수 안에 done()을 넣으면 done() 함수가 실행되기 전에는 test를 끝내지 않는다.
async test with 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,reject) =>{
setTimeout(()=>{
resolve(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, reject) =>{
setTimeout(()=>{
resolve(new Error('error'));
}, 1000)
});
}
return expect(p()).rejects.toBeInstanceOf(Error);
});
})
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);
});
it('async-await, catch', async()=>{
function p() {
return new Promise ((resolve, reject) =>{
setTimeout(()=>{
resolve(new Error('error'));
}, 1000)
});
}
try {
await p();
} catch (error) {
expect(error).toBeInstanceOf(Error);
}
});
})
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} 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)
}
}
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.useFackTimers();
const { getByText } = render(<Button />);
fireEvent.click(buttonElement);
act(()=>{
// 5 초 흐른다.
jest.advanceTimersByTime(5000);
})
const p = getByText('버튼이 눌리지 않았다.');
expect(p).not.toBeNull();
expect(p).toBeInstanceOf(HTMLParagraphElement);
});
it('버튼을 클릭하면 5초동안 버튼이 비활성화 된다.', ()=>{
jest.useFackTimers();
const { getByText } = render(<Button />);
fireEvent.click(buttonElement);
// 비 활성화
// expect(buttonElement.disabled).toBeTruthy();
expect(buttonElement).toBeDisabled();
act(()=>{
// 5 초 흐른다.
jest.advanceTimersByTime(5000);
})
// 활성화
// expect(buttonElement.disabled).toBeFalsy();
expect(buttonElement).not.toBeDisabled();
const p = getByText('버튼이 눌리지 않았다.');
expect(p).not.toBeNull();
expect(p).toBeInstanceOf(HTMLParagraphElement);
});
});
: 필요할 때만 랜더!
랜더 전후의 일치 여부를 판단하는 규칙
서로 다른 타입의 두 엘리먼트는 서로 다른 트리를 만들어낸다.
개발자가 key prop을 통해, 여러 렌더링 사이에서 어떤 자식 엘리먼트가 변경되지 않아야 할지 표시해 줄 수 있다.
: shouldComponentUpdate를 활용하여 인자로 들어오는 previousProp와 this.props 를 비교하여 막는다.
클래스형 컴포넌트에서 React.PureComponent 를 사용하면 위와 같이 이전과 비교하여 다를때만 랜더하도록 한다.
props 를 비교하여 랜더를 결정하기 때문에 props 에 함수를 넣을때는 선언된 함수를 변수값으로 넣어야 매번 새롭게 함수가 선언되어 랜더가 일어나는것을 막을 수 있다.
함수형 컴포넌트에서 막기 (meme() 사용)
const Person = React.memo(({name, age})=>{
console.log("Person render");
return (
<div>
{name} / {age}
</div>
);
});
: 클래스형 컴포넌트의 React.PureComponent 처럼 이전 props 와 비교하여 변경사항이 없으면 랜더를 막는다.
const Person = React.memo(({name, age})=>{
console.log("Person render");
const toPersonClick = React.useCallback(()=>{},[]);
return (
<div onClick={toPersonClick}>
{name} / {age}
</div>
);
});
: useCallback() 함수의 두번째 인자인 의존성 부분을 빈 배열로 넣게되면 최소 실행시에만 첫번째 인자로 들어온 내용을 생성해준다. 의존성을 설정해줄 경우 의존하는 부분이 변경될때 새롭게 생성한다.
: 리엑트의 랜더 영역에 있지 않은 바깥쪽의 DOM 에 리액트 컴포넌트를 랜더할 수 있도록 한다.
import ReactDOM from "react-dom";
const Modal = ({children})=>{
return (
ReactDOM.createPortal(children, document.querySelector("#modal"))
);
}
export default Modal;
function App(){
const [visible, setVisible] = useState(false);
const open = () => {
setVisible(true)
}
const close = () => {
setVisible(false)
}
return (
<div>
<button onClick={open}>open</button>
{visible && <Modal>
<div style={
{ width:'100vw',
height: '100vh',
background: 'rgba(0,0,0,0.5)'}}
onClick={close}>Hello</div>
</Modal>}
</div>
)
}
export default App;