유닛 테스트 : 작은 단위로 테스트함
통합 테스트 : 여러 컴포넌트 들이 서로 상호작용 하면서 잘 하고 있다.
테스트 자동화 : 코드를 수정, 리팩토링 할 때에, 통합 테스트가 통과되는지를 확인하여 버그를 확인. 버그가 발생하는 상황을 테스트 자동화하여 버그 재발을 방지.
TDD - 실패, 성공, 리팩토링의 과정으로 진행된다.
여기서 '실패'는 일단 테스트 코드를 작성하고,
'성공'은 테스트 코드가 통과될 수 있도록 유닛을 수정하고,
리팩토링은 성공된 로직을 깔끔하게 수정하여 적용하는 것을 의미한다.
yarn add jest
yarn add @types/jest
pacakge.json 작성하고, yarn test로 실행
const stats = require('./stats');
describe('stats', () => {
it('가장 큰 값을 찾습니다.', () => {
expect(stats.max([1, 2, 3, 4])).toBe(4);
});
});
이때 'it' 키워드와 'test' 키워드는 동일한 역할을 한다.
대신 describe를 작성할 때 'it' 키워드는 '무엇을 할 것이다.' 'test'키워드는 '결과가 이러할 것이다.' 라고 작성한다.
특히 아직 작성하지 않은 코드에 대해서는 'it' 키워드를 쓰는 것이 문맥상 올바르다.
stats 파일 안에 max(Array)를 하면 갖아 큰 값을 찾아 반환한다는 테스트 코드이다.
stats.max라는 함수가 없으므로 실패가 된다.
2. max함수를 추가하고 테스트 코드가 성공이 되도록 작성한다.
stats.js
exports.max = numbers => {
let result = numbers[0];
for (i=0, i < numbers.length, i++) {
if (numbers[i] > result) {
result = n;
}
return result;
};
간단한 알고리즘을 이용해 구현했다. 테스트가 성공한다.
exports.max = numbers => {
let result = numbers[0];
numbers.forEach(n => {
if (n > result) {
result = n;
}
});
return result;
};
테스트에 성공한다.......
사실 자바스크립트에는 Math.max( , , ,...,)라는 함수가 있다.
exports.max = numbers => {
let result = Math.max(...numbers);
};
테스트에 성공한다.
describe('stats', () => {
it('gets maximum value', () => {
expect(stats.max([1, 2, 3, 4])).toBe(4);
});
it('gets minimum value', () => {
expect(stats.min([1, 2, 3, 4])).toBe(1);
});
it('gets average value', () => {
expect(stats.avg([1, 2, 3, 4, 5])).toBe(3);
});
});
이런 식으로 stats 모듈의 3개의 유닛에 대한 테스트가 작성된다.
react-testing-library는 props나 state에 대한 테스트는 진행하지 않는다. dom 단위로, 간결하고 일관된 테스트 코드 작성을 유도한다.
벨로퍼트님 자료와 현재 테스팅 라이브러리가 다르다.
@testing-library로 설치해야 한다.
yarn add @testing-library/react @testing-library/jest-dom @types/jest
설치를 완료하면 src/setupTests.js 파일이 생성된다.
src/setupTests.js
import "@testing-library/jest-dom";
import "@testing-library/jest-dom/extend-expect"; // jest-dom의 확장기능을 불러온다.
@testing-library는 cleanup-after-each가 기본값이다. 이를 원치 않으면 import '@testing-library/react/dont-cleanup-after-each';
를 삽입해주면 된다.
Profile 컴포넌트를 만들어 보자.
@/App.js
import React from "react";
import Profile from "./Profile";
const App = () => {
return <Profile username="velopert" name="김민준" />;
};
export default App;
@/Profile.js
import React from "react";
const Profile = ({ username, name }) => {
return (
<div>
<b>{username}</b>
<span>({name})</span>
</div>
);
};
export default Profile;
src/Profile.test.js
import React from "react";
import { render } from "@testing-library/react";
import Profile from "./Profile"; // 컴포넌트를 임포트함
describe("<Profile />", () => {
it("matches snapshot", () => {
const utils = render(<Profile username="velopert" name="김민준" />);
expect(utils.container).toMatchSnapshot();
});
it("shows the props correctly", () => {
const utils = render(<Profile username="velopert" name="김민준" />);
utils.getByText("velopert"); // velopert 라는 텍스트를 가진 엘리먼트가 있는지 확인
utils.getByText("(김민준)"); // (김민준) 이라는 텍스트를 가진 엘리먼트가 있는지 확인
utils.getByText(/김/); // 정규식 /김/ 을 통과하는 엘리먼트가 있는지 확인
});
});
PASS src/Profile.test.js
<Profile />
√ matches snapshot (20 ms)
√ shows the props correctly (12 ms)
› 1 snapshot written.
Snapshot Summary
› 1 snapshot written from 1 test suite.
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 1 written, 1 total
Time: 3.768 s
Ran all test suites related to changed files.
Watch Usage: Press w to show more.
테스트를 실행하고 나면 @/__snapshots__
폴더가 생성되어 있다.
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<Profile /> matches snapshot 1`] = `
<div>
<div>
<b>
velopert
</b>
<span>
(
김민준
)
</span>
</div>
</div>
`;
위 코드에서 toMatchSnapshot()은 스냅샷 테스트를 위해 사용된다.
유의할 점은 class 명 등도 스냅샷 된다.
import React from "react";
import styled from "styled-components";
const Profile = ({ username, name }) => {
return (
<div>
<b style={{ text: "white" }}>{username}</b>
<StyledSpan>({name})</StyledSpan>
</div>
);
};
export default Profile;
const StyledSpan = styled.span`
background-color: black;
`;
b태그에는 인라인 스타일을
span태그에는 styled-components를 적용했다.
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<Profile /> matches snapshot 1`] = `
<div>
<div>
<b>
velopert
</b>
<span
class="sc-bcXHqe hgnsif"
>
(
김민준
)
</span>
</div>
</div>
`;
styled-components에 클래스명이 생성되었다.
위 예제에서 getBy로 시작하는 놈들을 쿼리라고 부른다.
자주 쓰이는 쿼리는 아래와 같다.
getByLabelText : 라벨이 특정 텍스트인 인풋 가져오기
getByPlaceholderText : 플레이스 홀더가 특정 텍스트인 인풋 가져오기
getByText : 내용이 특정 텍스트인 요소 가져오기
getByDisplayValue : 'value'어트리뷰트가 특정 값인 요소 가져오기
getByAltText
getByTitle : 'title' 어트리뷰트가 특정 값인 요소 가져오기
getByRole : 'role'어트리뷰트가 특정 값인 요소 가져오기
getByTestId : 'data-testid'가 특정 값인 요소 가져오기
보다시피 getByTestId가 가장 마지막에 있다. 되도록이면 사용을 자제하는 것이 좋다.
@/App.js
import React from 'react';
import Counter from './Counter';
const App = () => {
return <Counter />;
};
export default App;
@/Counter.js
import React, { useState, useCallback } from 'react';
const Counter = () => {
const [number, setNumber] = useState(0);
const onIncrease = useCallback(() => {
setNumber(number + 1);
}, [number]);
const onDecrease = useCallback(() => {
setNumber(number - 1);
}, [number]);
return (
<div>
<h2>{number}</h2>
<button onClick={onIncrease}>+1</button>
<button onClick={onDecrease}>-1</button>
</div>
);
};
export default Counter;
@/Counter.test.js
import React from 'react';
import { render, fireEvent } from "@testing-library/react";
import Counter from './Counter';
describe('<Counter />', () => {
it('matches snapshot', () => {
const utils = render(<Counter />);
expect(utils.container).toMatchSnapshot();
});
it('숫자가 하나고 버튼이 두개인지', () => {
const utils = render(<Counter />);
// 버튼과 숫자가 있는지 확인
utils.getByText('0');
utils.getByText('+1');
utils.getByText('-1');
});
it('increases', () => {
const utils = render(<Counter />);
const number = utils.getByText('0');
const plusButton = utils.getByText('+1');
// 클릭 이벤트를 두번 발생시키기
fireEvent.click(plusButton);
fireEvent.click(plusButton);
expect(number).toHaveTextContent('2'); // jest-dom 의 확장 matcher 사용
expect(number.textContent).toBe('2'); // textContent 를 직접 비교
});
it('decreases', () => {
const utils = render(<Counter />);
const number = utils.getByText('0');
const plusButton = utils.getByText('-1');
// 클릭 이벤트를 두번 발생시키기
fireEvent.click(plusButton);
fireEvent.click(plusButton);
expect(number).toHaveTextContent('-2'); // jest-dom 의 확장 matcher 사용
});
});
fireEvent.이벤트이름(DOM, 이벤트객체);
가령 onChange 이벤트의 경우, 이벤트 객체를 통해 value 값을 넘겨주어야 한다.
fireEvent.change(myInput, { target: { value: 'hello world' } });
해당 이벤트를 발생시킨 후, 요소.toHaveTextContent('기대하는 텍스트')
matcher함수에 맞게 myInput.toHaveTextContent('hello world')로 테스트하면 된다.