테스트 기반 React.js 개발: Enzyme 및 Jest를 사용한 React.js 단위 테스트
게시 됨: 2022-03-11Michael Feathers에 따르면 테스트가 없는 코드는 레거시 코드라고 합니다. 따라서 레거시 코드 생성을 방지하는 가장 좋은 방법 중 하나는 테스트 주도 개발(TDD)을 사용하는 것입니다.
JavaScript 및 React.js 단위 테스트에 사용할 수 있는 도구가 많이 있지만 이 게시물에서는 Jest 및 Enzyme을 사용하여 TDD를 사용하여 기본 기능이 있는 React.js 구성 요소를 만듭니다.
TDD를 사용하여 React.js 구성 요소를 만드는 이유는 무엇입니까?
TDD는 코드에 많은 이점을 제공합니다. 높은 테스트 범위의 장점 중 하나는 코드를 깨끗하고 기능적으로 유지하면서 손쉬운 코드 리팩토링을 가능하게 한다는 것입니다.
이전에 React.js 구성 요소를 만든 적이 있다면 코드가 정말 빠르게 성장할 수 있다는 것을 깨달았을 것입니다. 상태 변경 및 서비스 호출과 관련된 문으로 인해 복잡한 조건이 많이 채워집니다.
단위 테스트가 없는 모든 구성 요소에는 유지 관리가 어려워지는 레거시 코드가 있습니다. 프로덕션 코드를 만든 후에 단위 테스트를 추가할 수 있습니다. 그러나 테스트했어야 하는 일부 시나리오를 간과할 위험이 있습니다. 먼저 테스트를 생성하면 구성 요소의 모든 논리 시나리오를 다룰 가능성이 높아져 리팩토링 및 유지 관리가 쉬워집니다.
React.js 구성 요소를 어떻게 단위 테스트합니까?
React.js 구성 요소를 테스트하는 데 사용할 수 있는 많은 전략이 있습니다.
- 특정 이벤트가 전달될 때
props
의 특정 함수가 호출되었는지 확인할 수 있습니다. - 또한 현재 구성 요소의 상태가 주어지면
render
기능의 결과를 가져와 미리 정의된 레이아웃과 일치시킬 수 있습니다. - 구성 요소의 자식 수가 예상 수량과 일치하는지 확인할 수도 있습니다.
이러한 전략을 사용하기 위해 React.js에서 테스트 작업에 편리한 두 가지 도구인 Jest와 Enzyme을 사용할 것입니다.
Jest를 사용하여 단위 테스트 만들기
Jest는 React.js와 잘 통합된 Facebook에서 만든 오픈 소스 테스트 프레임워크입니다. Jasmine 및 Mocha가 제공하는 것과 유사한 테스트 실행을 위한 명령줄 도구가 포함되어 있습니다. 또한 구성이 거의 없는 모의 함수를 생성할 수 있으며 주장을 더 쉽게 읽을 수 있는 정말 멋진 매처 세트를 제공합니다.
또한 구성 요소 렌더링 결과를 확인하고 확인하는 데 도움이 되는 "스냅샷 테스트"라는 정말 좋은 기능을 제공합니다. 스냅샷 테스트를 사용하여 구성 요소의 트리를 캡처하고 렌더링 트리(또는 첫 번째 인수로 expect
함수에 전달하는 모든 항목)와 비교하는 데 사용할 수 있는 파일에 저장합니다.
효소를 사용하여 React.js 구성 요소 탑재
Enzyme은 React.js 구성 요소 트리를 탑재하고 통과하는 메커니즘을 제공합니다. 이렇게 하면 주장을 실행하기 위해 자체 속성과 상태 및 자식 소품에 액세스하는 데 도움이 됩니다.
Enzyme은 구성요소 실장을 위한 두 가지 기본 기능인 shallow
및 mount
을 제공합니다. shallow
함수는 루트 구성 요소만 메모리에 로드하는 반면 mount
는 전체 DOM 트리를 로드합니다.
Enzyme과 Jest를 결합하여 React.js 구성 요소를 마운트하고 이에 대한 어설션을 실행할 것입니다.
환경 설정
이 예제를 실행하기 위한 기본 구성이 있는 이 리포지토리를 살펴볼 수 있습니다.
다음 버전을 사용하고 있습니다.
{ "react": "16.0.0", "enzyme": "^2.9.1", "jest": "^21.2.1", "jest-cli": "^21.2.1", "babel-jest": "^21.2.0" }
TDD를 사용하여 React.js 구성 요소 만들기
첫 번째 단계는 효소의 얕은 기능을 사용하여 React.js 구성 요소를 렌더링하려고 시도하는 실패한 테스트를 만드는 것입니다.
// MyComponent.test.js import React from 'react'; import { shallow } from 'enzyme'; import MyComponent from './MyComponent'; describe("MyComponent", () => { it("should render my component", () => { const wrapper = shallow(<MyComponent />); }); });
테스트를 실행한 후 다음 오류가 발생합니다.
ReferenceError: MyComponent is not defined.
그런 다음 테스트를 통과하기 위한 기본 구문을 제공하는 구성 요소를 만듭니다.
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div />; } }
다음 단계에서는 구성 요소가 Jest의 toMatchSnapshot
기능을 사용하여 미리 정의된 UI 레이아웃을 렌더링하는지 확인합니다.
이 메서드를 호출한 후 Jest는 __snapshots__
폴더에 추가되는 [testFileName].snap
이라는 스냅샷 파일을 자동으로 생성합니다.
이 파일은 구성 요소 렌더링에서 기대하는 UI 레이아웃을 나타냅니다.
그러나 순수 TDD를 수행하려고 하는 경우 이 파일을 먼저 생성한 다음 toMatchSnapshot
함수를 호출하여 테스트가 실패하도록 해야 합니다.
Jest가 이 레이아웃을 나타내기 위해 어떤 형식을 사용하는지 모르기 때문에 이것은 약간 혼란스럽게 들릴 수 있습니다.
toMatchSnapshot
함수를 먼저 실행하고 스냅샷 파일에서 결과를 보고 싶은 유혹을 받을 수 있으며 이는 유효한 옵션입니다. 그러나 순수 TDD를 사용하려면 스냅샷 파일이 구조화되는 방식을 배워야 합니다.
스냅샷 파일에는 테스트 이름과 일치하는 레이아웃이 포함되어 있습니다. 이는 테스트에 다음 형식이 있는 경우를 의미합니다.
desc("ComponentA" () => { it("should do something", () => { … } });
내보내기 섹션에서 이를 지정해야 합니다. Component A should do something 1
.
여기에서 스냅샷 테스트에 대한 자세한 내용을 읽을 수 있습니다.
따라서 먼저 MyComponent.test.js.snap
파일을 만듭니다.
//__snapshots__/MyComponent.test.js.snap exports[`MyComponent should render initial layout 1`] = ` Array [ <div> <input type="text" /> </div>, ] `;
그런 다음 스냅샷이 구성 요소 자식 요소와 일치하는지 확인하는 단위 테스트를 만듭니다.
// MyComponent.test.js ... it("should render initial layout", () => { // when const component = shallow(<MyComponent />); // then expect(component.getElements()).toMatchSnapshot(); }); ...
render 메소드의 결과로 components.getElements
를 고려할 수 있습니다.
스냅샷 파일에 대한 검증을 실행하기 위해 이러한 요소를 expect
메소드에 전달합니다.
테스트를 실행한 후 다음 오류가 발생합니다.

Received value does not match stored snapshot 1. Expected: - Array [ <div> <input type="text” /> </div>, ] Actual: + Array []
Jest는 component.getElements
의 결과가 스냅샷과 일치하지 않는다고 말합니다. 따라서 MyComponent
에 입력 요소를 추가하여 이 테스트를 통과하도록 합니다.
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div><input type="text" /></div>; } }
다음 단계는 값이 변경될 때 함수를 실행하여 input
에 기능을 추가하는 것입니다. onChange
소품에 함수를 지정하여 이를 수행합니다.
테스트가 실패하도록 먼저 스냅샷을 변경해야 합니다.
//__snapshots__/MyComponent.test.js.snap exports[`MyComponent should render initial layout 1`] = ` Array [ <div> <input onChange={[Function]} type="text" /> </div>, ] `;
스냅샷을 먼저 수정하는 것의 단점은 props(또는 속성)의 순서가 중요하다는 것입니다.
Jest는 스냅샷에 대해 확인하기 전에 expect
함수에서 받은 소품을 알파벳순으로 정렬합니다. 따라서 순서대로 지정해야 합니다.
테스트를 실행한 후 다음 오류가 발생합니다.
Received value does not match stored snapshot 1. Expected: - Array [ <div> onChange={[Function]} <input type="text”/> </div>, ] Actual: + Array [ <div> <input type=”text” /> </div>, ]
이 테스트를 통과하려면 onChange
에 빈 함수를 제공하면 됩니다.
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div><input onChange={() => {}} type="text" /></div>; } }
그런 다음 onChange
이벤트가 전달된 후 구성 요소의 상태가 변경되는지 확인합니다.
이를 위해 UI에서 실제 이벤트를 모방하기 위해 이벤트 를 전달하여 입력에서 onChange
함수를 호출할 새 단위 테스트를 만듭니다.
그런 다음 구성 요소 상태 에 input
이라는 키가 포함되어 있는지 확인합니다.
// MyComponent.test.js ... it("should create an entry in component state", () => { // given const component = shallow(<MyComponent />); const form = component.find('input'); // when form.props().onChange({target: { name: 'myName', value: 'myValue' }}); // then expect(component.state('input')).toBeDefined(); });
이제 다음 오류가 발생합니다.
Expected value to be defined, instead received undefined
이는 구성 요소에 input
이라는 상태의 속성이 없음을 나타냅니다.
구성 요소의 상태에서 이 항목을 설정하여 테스트를 통과합니다.
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div><input onChange={(event) => {this.setState({input: ''})}} type="text" /></div>; } }
그런 다음 새 상태 항목에 값이 설정되어 있는지 확인해야 합니다. 이벤트에서 이 값을 얻습니다.
상태에 이 값이 포함되어 있는지 확인하는 테스트를 만들어 보겠습니다.
// MyComponent.test.js ... it("should create an entry in component state with the event value", () => { // given const component = shallow(<MyComponent />); const form = component.find('input'); // when form.props().onChange({target: { name: 'myName', value: 'myValue' }}); // then expect(component.state('input')).toEqual('myValue'); }); ~~~ Not surprisingly, we get the following error. ~~ Expected value to equal: "myValue" Received: ""
마지막으로 이벤트에서 값을 가져와 입력 값으로 설정하여 이 테스트를 통과합니다.
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div><input onChange={(event) => { this.setState({input: event.target.value})}} type="text" /></div>; } }
모든 테스트를 통과한 후 코드를 리팩토링할 수 있습니다.
onChange
prop에 전달된 함수를 updateState
라는 새 함수로 추출할 수 있습니다.
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { updateState(event) { this.setState({ input: event.target.value }); } render() { return <div><input onChange={this.updateState.bind(this)} type="text" /></div>; } }
이제 TDD를 사용하여 생성된 간단한 React.js 구성 요소가 있습니다.
요약
이 예에서 우리는 테스트에 실패하고 통과할 수 있는 최소한의 코드를 작성하는 모든 단계를 수행하여 순수한 TDD를 사용하려고 했습니다.
일부 단계는 불필요해 보일 수 있으며 건너뛰고 싶을 수 있습니다. 그러나 단계를 건너뛸 때마다 덜 순수한 버전의 TDD를 사용하게 됩니다.
덜 엄격한 TDD 프로세스를 사용하는 것도 유효하며 잘 작동할 수 있습니다.
당신을 위한 나의 추천은 어떤 단계도 건너뛰는 것을 피하고 어렵다고 느끼더라도 기분이 나빠지지 않는 것입니다. TDD는 마스터하기 쉽지 않은 기술이지만 확실히 할 가치가 있습니다.
TDD 및 관련 BDD(행동 주도 개발)에 대해 자세히 알아보려면 동료 Toptaler Ryan Wilcox의 상사가 TDD를 인정하지 않음을 읽어보세요.