[개발] React 테스트 코드 작성하기
** 이 글은 필자의 Medium 포스팅을 포팅한 것 입니다
Enzyme을 활용하여 React 컴포넌트 테스팅 코드 작성하기
이번 포스팅에서는 Airbnb에서 오픈소스화 한 React 테스팅 유틸리티인 Enzyme을 활용하여 React 컴포넌트 테스팅 코드 작성하는 법을 알아보도록 하겠다.
Enzyme is a JavaScript Testing utility for React that makes it easier to assert, manipulate, and traverse your React Components’ output.
React는 자체적인 테스팅 유틸리티를 제공하고 있으나, 공식 문서에서도 Airbnb의 Enzyme을 사용하도록 권장하고 있을 정도로 상당히 편리하고 직관적인 API를 제공하고 있다. 세부적인 API reference는 Enzyme 공식 문서를 통해서 확인할 수 있으니, 실제적으로 적용할 수 있는 예제 위주로 글을 풀어나갈 것이며 Mocha와 Chai에 대한 상세한 설명은 생략하도록 하겠다.
Enzyme 세팅하기
Enzyme은 기존의 테스트 환경에서 enzyme
모듈만 추가해주면 간단하게 설정이 가능하다.
npm i --save-dev enzyme
enzyme
모듈 설치 후 테스트 파일에서 필요한 API를 불러와 아래와 같이 컴포넌트 테스트에 활용할 수 있다.
/* ... */
import { shallow } from 'enzyme';
describe('<Foo />', () => {
it('should render one <div>', () => {
const wrapper = shallow(<Foo />);
expect(wrapper.find('div')).to.have.lengthOf(1);
});
});
렌더링 테스트하기
Enzyme을 통해서(혹은 기본 제공되는 ReactTestUtils을 통해) 테스트하는 가장 기본적인 항목은 해당 컴포넌트가 의도한대로 내용물을 그려주느냐 하는 것이다. 좀 더 쉽게 설명하면 render()
메소드가 리턴하는 요소를 테스트하는 것이다.
예시) 기본 중의 기본
/* ... */
import React from 'react';
import { shallow } from 'enzyme';
class Foo extends React.Component {
_something() {
/* DO SOMETHING */
}
render() {
return (
<div className="foo">
<span className="bar">baz</span>
</div>
);
}
}
describe('<Foo />', () => {
let wrapper;
beforeEach(() => { wrapper = shallow(<Foo />); });
it('should render one <div> with class foo', () => {
expect(wrapper.find('div.foo')).to.have.lengthOf(1);
});
it('should render one <span> with class bar', () => {
expect(wrapper.find('span.bar')).to.have.lengthOf(1);
});
it('should render baz inside <span> with class bar', () => {
expect(wrapper.find('span.bar').text()).to.be.equal('baz');
});
});
만약 render()
메소드 안에 부모 컴포넌트에서 주어진 props
에 따라 조건부로 엘리먼트를 그려줘야 한다면 다음과 같이 테스트 할 수 있다.
예시) props
에 따른 조건부 렌더링 테스트하기
/* ... */
class Foo extends React.Component {
/* ... */
render() {
if (this.props.shouldDrawFoo) {
return (
<div className="foo">
foo
</div>
);
}
return (
<div className="bar">
bar
</div>
);
}
}
describe('<Foo />', () => {
it('should render one <div> with class foo', () => {
const wrapper = shallow(<Foo shouldDrawFoo={true} />); // Pass prop inside shallow method call
expect(wrapper.find('div.foo').text()).to.be.equal('foo');
});
it('should render one <div> with class bar', () => {
const wrapper = shallow(<Foo shouldDrawFoo={false} />); // Pass prop inside shallow method call
expect(wrapper.find('div.bar').text()).to.be.equal('bar');
});
});
div
나 span
같은 DOM 엘리먼트 뿐만 아니라 동일한 React 컴포넌트 역시 같은 방법으로 테스트가 가능하다.
예시) 또 다른 컴포넌트를 그리는지 테스트하기
/* ... */
import Bar from './bar';
class Foo extends React.Component {
/* ... */
render() {
return (
<Bar />
);
}
}
describe('<Foo />', () => {
it('should render <Bar />', () => {
const wrapper = shallow(<Foo />);
expect(wrapper.find(Bar)).to.have.lengthOf(1);
});
});
Method 테스트하기
Webpack을 이용하여 ES2015의 클래스 문법을 사용하여 React 컴포넌트를 작성할 때, 비즈니스 로직을 포함한 메소드를 만드는 경우가 많다. 컴포넌트를 테스트할 때, 단순한 렌더링 테스트 뿐만 아니라 이러한 메소드 역시 테스트 해줄 필요가 있다. 메소드만 독립적으로 테스트 하는 방법은 크게 두가지가 있다.
1. 프로토타입을 활용하는 방법
아시다시피 ES2015의 클래스는 본질적으로 자바스크립트의 프로토타입 상속 체계를 기반으로 만들어진 Syntactic sugar이다. 따라서 해당 클래스 안에 만들어진 메소드는 그 클래스의 프로토타입을 통해 접근하고 테스트 할 수 있다. 아래의 예시를 참고 하도록 하자.
예시) 프로토타입을 이용한 메소드 테스트
/* ... */
class Foo extends React.Component {
/* ... */
test() {
return 'foo';
}
render() { /* ... */ }
}
describe('<Foo />', () => {
describe('#test', () => {
const testFunc = Foo.prototype.test;
it('should return foo', () => {
expect(testFunc()).to.be.eq('foo');
});
});
});
다만 이 방법은 해당 메소드가 this
키워드로 컴포넌트의 state
나 props
에 접근하고자 할 때 의도한대로 동작을 하지 않는 문제가 있어 추천하지 않는다.
예시) this
키워드가 컴포넌트 인스턴스를 가르키지 않는다
/* ... */
class Foo extends React.Component {
state = { test: 'test' }
/* ... */
test() {
return this.state.test;
}
render() { /* ... */ }
}
describe('<Foo />', () => {
describe('#test', () => {
const testFunc = Foo.prototype.test;
it('should return foo', () => {
expect(testFunc()).to.be.eq('foo'); // Error!! this.state is undefined!!
});
});
});
Enzyme은 이러한 문제를 위해 해당 React 컴포넌트의 인스턴스로 접근하는 API를 제공하고 있다.
2. Enzyme의 instance 메소드 활용하기
컴포넌트의 메소드를 테스트하는 다른 방법으로는 Enzyme의 instance()
API를 이용하는 것이다. shallow
나 mount
를 이용하여 ReactWrapper를 생성한 후에 instance()
메소드를 호출해줌으로써 해당 React 컴포넌트의 인스턴스에 접근할 수 있게 된다. 그 후에 해당 인스턴스의 메소드를 테스트하게 되면 this
키워드가 의도한대로 작동하게 되어 문제없이 테스트를 진행할 수 있다.
예시) instance API 사용하기
/* ... */
class Foo extends React.Component {
/* ... */
test() {
return 'foo';
}
render() { /* ... */ }
}
describe('<Foo />', () => {
describe('#test', () => {
const instance = shallow(<Foo />).instance(); // Use instance() API of Enzyme
it('should return foo', () => {
expect(instance.test()).to.be.eq('foo');
});
});
});
Lifecycle 테스트하기
라이프사이클은 React로 어플리케이션을 만들 때, 빈번하게 사용되는 기능인데, 이것 역시 Enzyme으로 손쉽게 테스트 할 수 있다. 필자는 본 섹션의 예시 코드에서 특정 메소드의 호출 여부를 확인할 수 있도록 도와주는 sinon.js를 활용하도록 하겠다. 예를 들어 아래와 같은 컴포넌트가 있다고 가정하자.
예시) 라이프사이클 훅에서 특정 메소드를 호출하는 컴포넌트
class Foo extends React.Component {
componentDidMount() {
this.props.callback();
}
/* ... */
render() { /* ... */ }
}
componentDidMount
hook에서 props
로 주어진 callback
함수를 한번 호출하는 컴포넌트이다. callback
함수의 동작 자체는 따로 테스트를 하면 될 것이고, 여기서는 해당 컴포넌트가 마운트 된 후에 해당 함수가 호출이 되었는지를 테스트 할 필요가 있다.
예시) 함수에 스파이를 부착하여 넘김으로써 해당 함수의 호출 여부를 테스트 할 수 있다
describe('<Foo />', () => {
describe('#componentDidMount', () => {
it('should return foo', () => {
const cb = sinon.spy();
mount(<Foo callback={cb} />);
expect(cb.calledOnce).to.be.eq(true);
});
});
});
mount vs. shallow
그리고 위의 예시에서는 이전의 테스트 코드에서 사용하던 shallow
API 대신에 mount
를 사용하였는데, 두 API는 유사한 동작을 하나 mount
가 좀 더 실제 컴포넌트의 동작과 유사한 수준의 동작을 한다. 공식 문서에서는 테스트 해야할 컴포넌트가 DOM API와 인터랙션하거나 라이프사이클 훅을 모두 활용하는 경우에 mount
API의 사용을 가이드하고 있다.
특히 라이프사이클 훅 호출 여부는 mount
API와 shallow
API가 상이하므로 유의가 필요하다. 간단하게 정리하면 mount
API는 모든 라이프사이클 훅이 호출되고 shallow
API는 componentDidMount
와 componentDidUpdate
를 제외하고 라이프사이클 훅이 호출된다. 이러한 차이점에 유의하여 선별적으로 API를 사용하도록 하자.
React 테스트하기…
React는 Enzyme과 같은 유틸리티의 도움으로 컴포넌트 단위로 유닛 테스트가 상당히 용이하다. 비록 완벽한 브라우저 상의 테스트는 아닐지라도 이런 유틸리티 덕분에 렌더링과 주요 비즈니스 로직 등을 테스트하기에 부족함이 없기에 React로 만든 웹앱은 테스트가 상당히 용이한 편이다. (Angular 1.x의 테스트와 비교하면…) 상대적으로 적은 노력으로 큰 효과를 얻을 수 있다는 점이 많은 사람들로 하여금 React를 사용하게 만드는 한가지 이유라고 생각한다.
**스팀잇에 처음으로 쓰는 글이 개발 관련 글이네요...아직 익숙하지가 않아서 기존 글을 포팅하는 수준이지만, 계속해서 컨텐츠를 생산해볼까 합니다. 응원 및 조언 부탁드립니다!