본문 바로가기

React

jest - enzyme

테스트 주도 개발 

 

jest와 enzyme

 

기존에 보일러플레이트를 만들었던 코드에서 시작. CRA로 시작해도 무방합니다

 

npm i -D enzyme enzyme-adapter-react-16

설치 후 src 폴더에 setupTests.js 파일 생성

 

import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

configure({ adapter: new Adapter() });

 

스냅샷 테스팅

 

렌더링된 결과가 이전에 렌더링한 결과와 일차하는지 확인하는 작업

enzyme에서 테스팅 하려면 enzyme-to-json 설치

npm i -D enzyme-to-json

 

그 다음 package.json폴더에

 

test 스크립트, jest 설정

 

...

"test": "jest"
},
"jest": {
    "setupFilesAfterEnv": ["./src/setupTests.js"],
    "snapshotSerializers": ["enzyme-to-json/serializer"]
  },
...

 

setupFilesAfterEnv: 보시는바와같이 테스트의 셋업파일을 명시

(제가 사용하는 enzyme-adapter-react-16 1.14.0 버전은 요걸 사용해야 에러가 안남.)

 

snapshotSerializers: 스냅샷 변환기 enzyme-to-json 사용

 

src폴더에 Profile.js파일 생성

 

import React from 'react';

const Profile = ({ id, name }) => {
  return (
    <div>
      <b>{id}</b>&nbsp;
      <span>{name}</span>
    </div>
  );
};

export default Profile;

 

test 폴더 생성 후 Profile.test.js 파일 생성

 

enzyme에서는 컴포넌트 접근이 가능하다 테스트명 '렌더 props 비교'

import React from 'react';
import { mount } from 'enzyme'
import Profile from '../src/Profile'

describe('<Profile />', () => {
   it('스냅샷 비교', () => {
      const wrapper = mount(<Profile id={'id'} name={'kim'} />);
      expect(wrapper).toMatchSnapshot();
   });
   it('렌더 props 비교', () => {
      const wrapper = mount(<Profile id={'id'} name={'kim'} />);
      expect(wrapper.props().id).toBe('id');
      expect(wrapper.props().name).toBe('kim');

      const boldElement = wrapper.find('b');		// find(): 특정 DOM 선택 가능 querySelector같음
      expect(boldElement.contains('id')).toBe(true); //contains(): innerText같은 안의 값이 id인지
      const spanElement = wrapper.find('span');
      expect(spanElement.text()).toBe('kim');	//text(): span의 innerText 같은 기능.

   })
})

enzyme mount라는 함수는 Enzyme을 통하여 리액트 컴포넌트를 렌더링 해줍니다.

이를 통해서 만든 wrapper로 props, state, DOM 조회할 수 있따.

 

npm test를 실행하면 test 폴더에 __snapshots__/Profile.test.js.snap 파일이 생기는데, 추후에 컴포넌트를

수정하게 되면 테스트가 실패한다.

 

카운터 앱 테스트 (클래스, 함수형)

 

import React, { Component } from 'react';

class Counter extends Component {
   state = {
      number: 0
   }
   handleIncrease = () => {
      this.setState({
        number: this.state.number + 1
      });
    };
    handleDecrease = () => {
      this.setState({
        number: this.state.number - 1
      });
    };

   render() {
      return (
         <div>
            <h2>{this.state.number}</h2>
            <button onClick={this.handleIncrease}>+1</button>
            <button id={'mb'} onClick={this.handleDecrease}>-1</button>
         </div>
      )
   }
}

export default Counter;

 

그 후 Counter.test.js

import React from 'react';
import { shallow, mount } from 'enzyme'
import Counter from '../src/Counter'

describe('카운터 테스트', () => {
   it('스냅샷 매치', () => {
      const wrapper = shallow(<Counter />)
      expect(wrapper).toMatchSnapshot();
   });
   it('초기 값', () =>  {
      const wrapper = shallow(<Counter />)
      expect(wrapper.state().number).toBe(0)
   })
   it('증가', () => {
      const wrapper = shallow(<Counter />)
      wrapper.instance().handleIncrease();
      expect(wrapper.state().number).toBe(1)
   })
   it('감소', () => {
      const wrapper = shallow(<Counter />)
      wrapper.instance().handleDecrease();
      expect(wrapper.state().number).toBe(-1)
   })
   it('증가 버튼 클릭 시뮬', () => {
      const wrapper = shallow(<Counter />)
      
      //태그 타입이 button이고 text가 String '+1'인 요소 찾기
      const plusButton = wrapper.findWhere(node =>
      	node.type() === 'button' && node.text() === '+1')
      plusButton.simulate('click');
      expect(wrapper.state().number).toBe(1)
   })
   it('감소 버튼 클릭 시뮬', () => {
      const wrapper = shallow(<Counter />)
      wrapper.find('#mb').simulate('click') // id가 mb인 요소를 찾아 클릭
      expect(wrapper.state().number).toBe(-1)
   })
})

Shallow 함수는 컴포넌트 내부에 또 다른 리액트 컴포넌트가 있다면 이를 렌더링 하지 않습니다.

 

 

리액트 훅 카운터 앱

 

참고로 Hooks는 반드시 mount를 사용, useEffect Hook은 shallow에서 작동하지 않고, 버튼 엘리먼트에 연결되어있는

함수가 이전 함수를 가르키고 있기 때문에, -1버튼의 클릭이벤트를 두번 simulate해도 결과값이 -2가 아니라 -1

 

import React, {useState} from 'react';

const HookCounter = ({}) => {
   const [number, setNumber] = useState(0);

   const onIncre = () => setNumber(number + 1)
   const onDecre = () => setNumber(number - 1)

   return (
      <div>
         <h2>{number}</h2>
         <button onClick={onIncre}>+1</button>
         <button id={'mb'} onClick={onDecre}>-1</button>
      </div>
   )
}

export default HookCounter;

함수 컴포넌트라면 “인스턴스”라는 개념이 존재하지 않습니다. 그러므로 상태와 생명주기가 있는 클래스 컴포넌트에서 훅을 사용한 컴포넌트로 리팩토링했을 때에는 이 테스트의 .instance() .state()가 동작하지 않을 겁니다.

https://edykim.com/ko/post/react-hooks-whats-going-to-happen-to-my-tests/

 

테스트는 요렇게 해당 요소의 값을 판단해야 테스트가 진행됩니다. (더 좋은 방법이 있을 수 있음)

it('감소 버튼 클릭 시뮬', () => {
       const wrapper = mount(<HookCounter />)
       wrapper.find('#mb').simulate('click')
       const number = wrapper.find('h2').text()
       expect(number).toBe("-1")
   })

text이기 때문에 스트링으로 넣어주어야 합니다.

 

출처: https://velog.io/@velopert/react-testing-with-enzyme

'React' 카테고리의 다른 글

리액트 스토리북 만들기  (0) 2019.06.25