도움말 - 글감 수집하기 (인용)

도움말 - 부분 리뷰 작성하기

Jest와 Enzyme을 이용한 리액트(React) 컴포넌트 테스트(Testing React components with Jest and Enzyme)

몇몇 사람들은 리액트(React) 컴포넌트를 테스트하는 것은 쓸모가 없다고 말하는 경우가 있다. 하지만 유용하다고 생각할 때가 몇 가지 있다:

  • 컴포넌트 라이브러리
  • 오픈 소스 프로젝트
  • 써드 파티(3rd party) 컴포넌트와의 통합
  • 버그 방지

나는 많은 도구들을 사용 해봤고, 마침내 다른 개발자들에게 제안 할 수있는 조합을 찾았다:

  • Jest, 테스트 러너
  • Enzyme, 리액트 테스트 유틸리티
  • Jest snapshot matcher를 위해 Enzyme 래퍼를 변환하는 enzyme-to-json

대부분의 테스트에서 Jest 스냅샷으로 얕은(shallow) 렌더링을 사용한다.


Jest 스냅샷 테스트

얕은(shallow) 렌더링

얕은 렌더링은 자식(children)을 제외한 컴포넌트 자체만 렌더링한다. 따라서, 자식 컴포넌트에서 무언가를 변경하면 출력(output)은 변경되지 않는다. 또는 자식 컴포넌트의 버그로 인해 컴포넌트 테스트가 중단되지 않는다. 또한 DOM이 필요없다.

예를 들어,이런 컴포넌트가 있을 때:


const ButtonWithIcon = ({icon, children}) => (
<button><Icon icon={icon} />{children}</button>
);

리액트에서는 이렇게 렌더링 된다:


<button>
<i class="icon icon_coffee"></i>
Hello Jest!
</button>

하지만 얕은 렌더링에서는 이렇게 렌더링 된다:


<button>
<Icon icon="coffee" />
Hello Jest!
</button>

Icon 컴포넌트가 렌더링되지 않은것에 주목하자.

스냅샷 테스트

Jest 스냅샷은 텍스트로 구성된 창(windows)과 버튼(button)이있는 오래된 텍스트 UI와 비슷하다. 이것은 텍스트 파일로 저장되는 렌더링 된 컴포넌트의 출력 결과이다.

Jest에게 이 컴포넌트의 출력이 절대로 의도치 않게(accidentally) 변경되면 안되고, 다음과 같은 파일에 이 파일을 저장 하도록 한다:


exports[`test should render a label 1`] = `
<label
className="isBlock">
Hello Jest!
</label>
`;
exports[`test should render a small label 1`] = `
<label
className="isBlock isSmall">
Hello Jest!
</label>
`;

마크업(markup)을 변경할 때마다 Jest는 diff를 보여주고 의도된 변경이라면 스냅 샷을 업데이트 할지 여부를 물어본다.

Jest는 테스트 외에 스냅 샷을 __snapshots __ /Label.spec.js.snap과 같은 파일로 저장하기 때문에 스냅샷 파일 역시 커밋을 해야한다.

왜 Jest인가

  • 매우 빠름
  • 스냅샷 테스트
  • 변경 사항에 관련된 테스트만 재실행하는 인터렉티브(interactive)한 감시 모드
  • 유용한 실패(fail) 메세지
  • 간단한 설정
  • 목(Mocks)과 스파이(spies)
  • 단일 커맨드 라인을 이용한 커버리지 리포트
  • 적극적(active)인 개발
  • Chai에서 expect(foo).to.be.a('function') 대신에 expect(foo).to.be.a.function과 같은 잘못된 assert를 쓰는 것은 불가능하다. 왜내하면, (correct)expect(foo).to.be.true 다음에  쓰는 것이 자연스럽기 때문이다.

왜 Enzyme인가

  • 얕은 렌더링, 정적 렌더링 된 마크 업 또는 DOM 렌더링을 사용하는 편리한 유틸리티
  • 엘리먼트를 찾고(find), props 등을 읽는 것이 jQuery API 유사 

세팅

나는 바벨(Babel)과 CSS 모듈을 사용하기 때문에 아래의 예제에서 볼 수 있지만 이는 선택 사항이다.

먼저 peer 디펜던시를 포함한 모든 디펜던시를 설치한다:


npm install --save-dev jest babel-jest react-addons-test-utils enzyme enzyme-to-json

package.json을 업데이트 한다:


"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
},
"jest": {
"setupFiles": ["./test/jestsetup.js"],
"snapshotSerializers": [
"<rootDir>/node_modules/enzyme-to-json/serializer"
]
}

snapshotSerializers를 사용하면 enzyme-to-json의 toJson 함수를 호출하여 수동으로 변환하지 않고도 Enzyme 래퍼(wrapper)를 Jest의 스냅샷에 직접 전달할 수 있다.

Jest 환경을 커스터마이즈(customize)하기 위해 jestsetup.js 파일을 작성한다 (위의 setupFiles 참조):


// Make Enzyme functions available in all test files without importing
import { shallow, render, mount } from 'enzyme';
global.shallow = shallow;
global.render = render;
global.mount = mount;
// Skip createElement warnings but fail tests on any other warning
console.error = message => {
if (!/(React.createElement: type should not be null)/.test(message)) {
throw new Error(message);
}
};

CSS 모듈의 경우 package.json의 jest 섹션에도 추가한다:


"jest": {
"moduleNameMapper": {
"^.+\\.(css|scss)quot;: "identity-obj-proxy"
}
}

그리고 아래를 실행한다.


npm install-save-dev identity-obj-proxy

identity-obj-proxy는 노드 버전 4와 5에 대해 node — harmony-proxies 플래그가 필요한 것에 유의하자.

테스트 작성

기본 컴포넌트 렌더링 테스트

대부분의 non-interactive 컴포넌트에는 충분하다:


it('should render a label', () => {
const wrapper = shallow(
<Label>Hello Jest!</Label>
);
expect(wrapper).toMatchSnapshot();
});
it('should render a small label', () => {
const wrapper = shallow(
<Label small>Hello Jest!</Label>
);
expect(wrapper).toMatchSnapshot();
});
it('should render a grayish label', () => {
const wrapper = shallow(
<Label light>Hello Jest!</Label>
);
expect(wrapper).toMatchSnapshot();
});

props 테스트

때로는 더 명시 적으로 테스트에서 실제 값을 보고 싶을 경우가 있다. 이 경우 Jest assertion을 사용하여 Enzyme API를 사용한다:


it('should render a document title', () => {
const wrapper = shallow(
<DocumentTitle title="Events" />
);
expect(wrapper.prop('title')).toEqual('Events');
});
it('should render a document title and a parent title', () => {
const wrapper = shallow(
<DocumentTitle title="Events" parent="Event Radar" />
);
expect(wrapper.prop('title')).toEqual('Events — Event Radar');
});

스냅샷을 사용할 수없는 경우도 있다. 예를 들어 랜덤 ID 또는 이와 유사한 경우 :


it('should render a popover with a random ID', () => {
const wrapper = shallow(
<Popover>Hello Jest!</Popover>
);
expect(wrapper.prop('id')).toMatch(/Popover\d+/);
});

이벤트(event) 테스트

클릭(click) 또는 변경(change)과 같은 이벤트(event)를 시뮬레이션 한 다음 컴포넌트를 스냅샷과 비교할 수 있다.


it('should render Markdown in preview mode', () => {
const wrapper = shallow(
<MarkdownEditor value="*Hello* Jest!" />
);
expect(wrapper).toMatchSnapshot();
wrapper.find('[name="toggle-preview"]').simulate('click');
expect(wrapper).toMatchSnapshot();
});

때로는 하위(child) 컴포넌트와 상호 작용(inseract)하여 컴포넌트를 테스트가 필요 할 때가 있다. 이를 위해서는 Enzyme의 mount 메쏘드(method)를 사용해서 적절한 DOM 렌더링이 필요하다:


it('should open a code editor', () => {
const wrapper = mount(
<Playground code={code} />
);
expect(wrapper.find('.ReactCodeMirror')).toHaveLength(0);
wrapper.find('button').simulate('click');
expect(wrapper.find('.ReactCodeMirror')).toHaveLength(1);
});

이벤트 핸들러(event handler) 테스트

이벤트 테스트와 비슷하지만 스냅샷을 사용하여 컴포넌트의 렌더링 된 출력을 테스트하는 대신 Jest의 mock 함수를 사용하여 이벤트 핸들러 자체를 테스트한다:


it('should pass a selected value to the onChange handler', () => {
const value = '2';
const onChange = jest.fn();
const wrapper = shallow(
<Select items={ITEMS} onChange={onChange} />
);
expect(wrapper).toMatchSnapshot();
wrapper.find('select').simulate('change', {
target: { value },
});
expect(onChange).toBeCalledWith(value);
});

JSX 뿐만 아니라

Jest 스냅샷은 JSON과 함께 작동하므로 컴포넌트를 테스트하는 것과 같은 방법으로 JSON을 반환하는 모든 함수를 테스트 할 수 있다:


it('should accept custom properties', () => {
const wrapper = shallow(
<Layout
flexBasis={0}
flexGrow={1}
flexShrink={1}
flexWrap="wrap"
justifyContent="flex-end"
alignContent="center"
alignItems="center"
/>
);
expect(wrapper.prop('style')).toMatchSnapshot();
});

디버깅 및 문제 해결

얕은 렌더링을 출력 하려면 Enzyme의 디버그 메소드를 사용한다:


const wrapper = shallow(/*~*/);
console.log(wrapper.debug());

테스트가 다음과 같이 coverage 플래그로 실패 할 때:


-<Button
+<Component

arrow function 컴포넌트를 일반 function으로 교체한다:


- export default const Button = ({ children }) => {
+ export default function Button({ children }) {

리소스

Chris Pojer, Max Stoiber 및 Anna Gerus에게 교정 및 의견을 보내 주셔서 감사합니다.

P. S. 저의 오픈 소스 프로젝트를 확인해보세요: React Styleguidist, 핫로드 된 dev 서버가있는 컴포넌트 스타일 가이드 제너레이터(generator).




이 글은 Artem Sapegin의 Testing React components with Jest and Enzyme을 번역한 글입니다. 전문 번역가가 아니라 오역이 있을 수 있습니다. 지적해주시면 수정하도록 하겠습니다. 원문은 아래에서 확인 할 수 있습니다.


리뷰