안녕하세요. 이번 시간에는 React 테스트에 대해 알아보겠습니다.
리액트도 하나의 프레임워크(리덕스와 리액트 라우터와 묶어서)로 사용되는 만큼 테스트가 꼭 필요합니다. 대규모 애플리케이션이 되면 어디에서 에러가 발생할 지 예측하기 힘들어지기 때문이죠. 자바스크립트에는 많은 테스트 라이브러리나 프레임워크가 있지만, 리액트에서는 jest와 enzyme을 가장 많이 사용합니다.
jest는 테스트 프레임워크고, enzyme은 테스트 라이브러리입니다. jest가 좀 더 큰 개념이죠. enzyme을 jest와 함께 사용할 수도 있습니다. 이번 시간에는 그냥 같이 사용해서 둘 다 간단하게 배워봅시다. babel-jest와 babel-core는 테스트 할 때 ES2015+ 문법을 사용하기 위해 설치합니다. jest는 리액트뿐만 아니라 노드 등을 테스트할 때도 사용할 수 있으므로 알아두시는 게 좋습니다.
npm install --save-dev jest babel-jest babel-core enzyme enzyme-adapter-react-16
바벨7과 같이 사용하고 싶다면 @babel/core과 babel-core@7.0.0-0도 같이 설치해야 합니다.
npm을 통해 설치해줍니다. 개발용으로만 쓰이기 때문에 --save-dev또는 -D입니다.
테스트는 단위 테스트를 할 겁니다. 영어로는 Unit test라고 불리는데요. 한 가지 역할 단위로 테스트하는 방법입니다. 단일 책임 원칙에 따라서 한 가지 역할만 하면 충분합니다.
먼저 테스트할 간단한 카운터입니다.
counter.jsx
import React, { Component } from 'react';
export default class Counter extends Component {
state = {
value: 0,
title: '',
}
changeTitle = (e) => {
this.setState(() => ({ title: e.target.value }));
}
increment = () => {
this.setState(prevState => ({ value: prevState.value + 1 }));
};
render() {
return (
<div>
<input value={this.state.title} id="title" onChange={this.changeTitle} />
<b>{this.state.value}</b>
<button id="up" onClick={this.increment}>증가</button>
</div>
);
}
}
테스트 파일을 생성해봅시다.
counter-test.jsx
import React from 'react';
import Counter from './counter.jsx';
import { shallow, configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
configure({ adapter: new Adapter() });
describe('<Counter />', () => {
it('성공적으로 렌더링되어야 합니다.', () => {
const wrapper = shallow(<Counter />);
expect(wrapper.length).toBe(1);
});
it('타이틀 인풋이 렌더링되어야 합니다.', () => {
const wrapper = shallow(<Counter />);
expect(wrapper.find('#title').length).toEqual(1);
});
it('타이틀이 변경되어야 합니다.', () => {
const wrapper = shallow(<Counter />);
wrapper.find('#title').simulate('change', { target: { value: '값' } });
expect(wrapper.state().title).toBe('값');
});
it('숫자가 올라가야 합니다.', () => {
const wrapper = shallow(<Counter />);
wrapper.find('#up').simulate('click');
wrapper.find('#up').simulate('click');
expect(wrapper.state().value).toBeLessThan(1);
});
});
enzyme은 react@16과 react@15에서 다르게 동작하기 때문에 enzyme-adapter-react-16과 configure 메서드를 사용해 react@16을 사용하고 있음을 알려주어야 합니다.
describe와 it은 jest에서 제공하는 함수입니다. describe는 하나의 테스트 그룹이고, it은 그 안의 작은 단위 테스트입니다. 나중에 결과 화면에서 describe와 it에 넣어준 문자열이 어떻게 보이는지 알게 될 것입니다.
enzyme에는 adapter를 적용하는 configure를 제외하면 크게 세 가지 메소드가 있습니다. shallow, mount, render입니다. API
- shallow: 간단한 컴포넌트를 메모리 상에 렌더링합니다. 단일 컴포넌트를 테스트할 때 유용합니다.
- mount: HOC나 자식 컴포넌트까지 전부 렌더링합니다. 다른 컴포넌트와의 관계를 테스트할 때 유용합니다.
- render: 컴포넌트를 정적인 html로 렌더링합니다. 컴포넌트가 브라우저에 붙었을 때 html로 어떻게 되는지 판단할 때 사용합니다.
enzyme 메소드로 렌더링한 wrapper는 다양한 기능을 할 수 있습니다. 내부 state나 props에 접근할 수도 있고, 이벤트를 시뮬레이션할 수도 있습니다.
state에 접근하려면 wrapper.state()
를 하면 되고, props에 접근하려면 wrapper.props()
를 하면 됩니다. 이벤트를 시뮬레이션 하려면 다음과 같이 합니다.
wrapper.find('#up').simulate('click'); // 버튼 클릭
wrapper.find('#up').simulate('click'); // 버튼 클릭
console.log(wrapper.state().value); // 2
wrapper.find('#title').simulate('change', { target: { value: '값' } }); // 인풋에 값 입력
console.log(wrapper.state().title); // '값'
보통 이벤트를 발생시키면 state가 따라서 변합니다. enzyme은 이런 것까지 다 추적해주기 때문에 편리합니다.
shallow 대신 mount를 사용하면 더 다양한 기능을 쓸 수 있지만, 지금같이 간단한 컴포넌트는 shallow로도 충분합니다.
expect는 jest에서 제공하는 것으로 테스트값과 예상값이 일치하는지 여부를 판단합니다. 일치한다면 테스트가 성공한 것이고, 일치하지 않는다면 테스트가 실패한 것입니다. toBe, toEqual, toBeLessThan, toBeGreaterThan 등, 단순 일치뿐만 아니라 이상, 이하, 초과, 미만, truthy, falsy 등을 판단할 수도 있습니다. API
이제 콘솔에 jest를 입력하면 테스트를 진행합니다. 마지막 it을 틀리게 설정했기 때문에 테스트가 실패합니다.
Error: expect(received).toBeLessThan(expected)
Expected value to be less than:
1
Received:
2
1보다 작을 것을 예상했는데 2가 들어와서 에러가 난 것입니다. toBeLessThan을 toBeGreaterThan으로 수정하면 테스트가 성공합니다.
PASS counter-test.jsx
<Counter />
✓ 성공적으로 렌더링되어야 합니다.(11ms)
✓ 타이틀 인풋이 렌더링되어야 합니다. (15ms)
✓ 타이틀이 변경되어야 합니다. (22ms)
✓ 숫자가 올라가야 합니다. (25ms)
Test Suites: 1 passed, 1 total
Tests: 4 passed, 4 total
Snapshots: 0 total
Time: 1.015s
성공했습니다. describe과 it에 적어두었던 문자열이 어떤 테스트인지를 알려줍니다.
컴포넌트가 복잡해질수록 테스트의 중요성도 높아집니다. React jest와 enzyme과 함께라면 걱정 없습니다!