React TDD - Enzyme

박정호·2022년 8월 19일
0

TDD

목록 보기
3/6
post-thumbnail

😁 Enzyme

  • Enzyme을 사용하여 테스트 코드를 작성할 때는 컴포넌트의 내부 기능을 자주 접근

ex) 컴포넌트가 지니고 있는 props, state를 확인하고, 컴포넌트의 내장 메서드를 직저 호출하기도 한다.

1️⃣ 설치 및 세팅

1. CRA 생성

$ yarn create react-app react-enzyme-test
# 혹은 npx create-react-app react-enzyme-test

2. 라이브러리 설치

$ yarn add enzyme enzyme-adapter-react-16
# 또는 npm install --save enzyme enzyme-adapter-react-16

3. src/setupTest.js에 다음과 같은 코드 추가

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

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

4. src/Profile.js에는 다음과 같은 코드를 작성

import React from 'react';

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

5. app.js에서 Profile컴포넌트를 렌더링

import React from 'react';
import Profile from './Profile';

function App() {
  return (
    <div>
      <Profile username="houya" name="박정호" />
    </div>
  );
}
export default App;

2️⃣ 스냅샷 테스팅

  • 스냅샷 테스탕이란, 렌더링된 결과가 이전에 렌더링한 결과와 일치하는지 확인하는 작업

1. Enzyme에서 스냅샷 테스팅을 하기 위해 enzyme-to-json 이라는 라이브러리를 설치

$ yarn add enzyme-to-json

2.package.json에는 'jest'설정 추가

{
	...
  },
  "jest": {
    "snapshotSerializers": ["enzyme-to-json/serializer"]
  }
}

3. Profile.test.js에 다음과 같은 코드 추가

  • mount라는 함수는 Enzyme을 통하여 리액트 컴포넌트를 렌더링 해준다.
    -> 이를 통해 만든 wrapper를 통해서 props조회, DOM조회, state조회 등이 가능
import React from 'react';
import { mount } from 'enzyme';
import Profile from './Profile';

describe('<Profile />', () => {
  it('matches snapshot', () => {
    const wrapper = mount(<Profile username="velopert" name="김민준" />);
    expect(wrapper).toMatchSnapshot();
  });
});

4. yarn test를 하게 되면 test가 성곡적으로 pass되는 것을 확인 할 수 있고, src에 __snapshots__/Profile.test.js.snap/ 라는 파일이 생성
-> 말 그대로 코드의 스냅샷이 찍혀서 저장되었고, 이제 이 코드와 다른 형식의 코드가 test되면 에러를 발생시킨다.

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`<Profile /> matches snapshot 1`] = `
<Profile
  name="박정호"
  username="houya"
>
  <div>
    <b>
      houya
    </b>
     
    <span>
      (
      박정호
      )
    </span>
  </div>
</Profile>
`;

5. 만약 Profile.js에 다음과 같이 !를 추가할 경우 스냅샷의 코드와 다르므로 에러가 난다.
-> 만약 코드를 변경하고 스냅샷의 업데이트를 원한다면 콘솔창에 u를 입력하면 스냅샷에도 !가 찍힌 상태로 변경되고 pass가 출력된다.

import React from 'react';

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

export default Profile

3️⃣ props 접근

  • 만약 코드내의 인스턴스에 접근하고 싶다면?
    -> wrapper.props().인스턴스.toBe('인스턴스명')
import React from 'react';
import { mount } from 'enzyme';
import Profile from './Profile';

describe('<Profile />', () => {
  it('matches snapshot', () => {
    const wrapper = mount(<Profile username="houya" name="박정호" />);
    expect(wrapper).toMatchSnapshot();
  });
  it('renders username and name', () => {
    const wrapper = mount(<Profile username="houya" name="박정호" />);
    expect(wrapper.props().username).toBe('houya');
    expect(wrapper.props().name).toBe('박정호');
  });
});

4️⃣ DOM 확인

  • 만약 DOM에 택스트가 잘 나타나는지 확인하려면?
    -> find함수를 이용하여 DOM을 찾는다. querySelector와 마찬가지로, css 클래스, 아이디, 태그 값을 find함수 안에 넣어 조회가 가능하다.
import React from 'react';
import { mount } from 'enzyme';
import Profile from './Profile';

describe('<Profile />', () => {
  	...
  it('renders username and name', () => {
    const wrapper = mount(<Profile username="houya" name="박정호" />);
		...
    const boldElement = wrapper.find('b');
    expect(boldElement.contains('houya')).toBe(true);
    const spanElement = wrapper.find('span');
    expect(spanElement.text()).toBe('(박정호)');
  });
});

5️⃣ 클래스형 컴포넌트의 테스팅

1. 다음과 같은 코드가 있다.

<src/Counter.js>
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 onClick={this.handleDecrease}>-1</button>
      </div>
    );
  }
}
export default Counter;

2. test 파일에는 mount 대신 shallow라는 함수를 사용한다.
-> shallow는 컴포넌트 내부에 또다른 리액트 컴포넌트가 있다면 이를 렌더링 하지 않는다.

만약 Counter내에 다른 컴포넌트인 Profile컴포넌트가 존재한다.

<src/Counter.js>
import React, { Component } from 'react';

class Counter extends Component {
 ...
  render() {
    return (
      <div>
     	...
        <Profile></Profile>
      </div>
    );
  }
}
export default Counter;

shallow함수를 사용할 경우 스냅샷
-> shallow에서의 최상위 요소는 div

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`<Counter /> matches snapshot 1`] = `
<div>
  <h2>
    0
  </h2>
  <button
    onClick={[Function]}
  >
    +1
  </button>
  <button
    onClick={[Function]}
  >
    -1
  </button>
  <Profile
    name="김민준"
    username="velopert"
  />
</div>
`;

mount함수를 사용할 경우 스냅샷
-> Profile의 내부 내용까지 전부 렌더링

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`<Counter /> matches snapshot 1`] = `
<Counter>
  <div>
    <h2>
      0
    </h2>
    <button
      onClick={[Function]}
    >
      +1
    </button>
    <button
      onClick={[Function]}
    >
      -1
    </button>
    <Profile
      name="houya"
      username="velopert"
    >
      <div>
        <b>
          velopert
        </b>
         
        <span>
          (
          박정호
          )
        </span>
      </div>
    </Profile>
  </div>
</Counter>
`;

3. 컴포넌트 안에 내장 메서드를 호출할 때는 instance()함수를 호출하여 인스턴스를 조회 후 메서드 호출

wrapper.instance().handleIncrease();

4. 컴포넌트의 state를 조회할 때는 state()함수 사용

expect(wrapper.state().number).toBe(0);

6️⃣ DOM 이벤트 시뮬레이트

1. 버튼 클릭 이벤트를 시뮬레이트하여 기능이 잘 작동하는지 확인

<counter.test.js>
import React from 'react';
import { shallow } from 'enzyme';
import Counter from './Counter';

describe('<Counter />', () => {
  it('matches snapshot', () => {
    ...
  });
  it('has initial number', () => {
    ...
  });
  it('increases', () => {
    ...
  });
  it('decreases', () => {
  	...
  });
  it('calls handleIncrease', () => {
    // 클릭이벤트를 시뮬레이트하고, state 를 확인
    const wrapper = shallow(<Counter />);
    const plusButton = wrapper.findWhere(
      node => node.type() === 'button' && node.text() === '+1'
    );
    plusButton.simulate('click');
    expect(wrapper.state().number).toBe(1);
  });
  it('calls handleDecrease', () => {
    // 클릭 이벤트를 시뮬레이트하고, h2 태그의 텍스트 확인
    const wrapper = shallow(<Counter />);
    const minusButton = wrapper.findWhere(
      node => node.type() === 'button' && node.text() === '-1'
    );
    minusButton.simulate('click');
    const number = wrapper.find('h2');
    expect(number.text()).toBe('-1');
  });
});

2. findWhere()함수를 이용하여 버튼 태그를 선택

<findWhere()함수 사용>
const plusButton = wrapper.findWhere(
      node => node.type() === 'button' && node.text() === '+1'
    );
const minusButton = wrapper.findWhere(
      node => node.type() === 'button' && node.text() === '-1'
    );
<find()함수 사용>
const buttons = wrapper.find('button');
const plusButton = buttons.get(0); // 첫번째 버튼 +1
const minusButton = buttons.get(1); // 두번째 버튼 -1

3. onClick한 것이 대한 변화에 대해서 알기
-> 버튼에 이벤트를 시뮬레이트할 때에는 원하는 엘리먼트를 찾아 simulate()함수 사용

    plusButton.simulate('click');
    minusButton.simulate('click');

-> simulate()함수의 첫번째 파라미터에는 이벤트 이름, 두번째 파라미터에는 이벤트 객체를 넣을 수 있다. 만약 onChange를 사용해 입력되는 내용을 확인하기 위해 다음과 같이 작성

input.simulate('change', {
  target: {
    value: 'hello world'
  }
});

8️⃣ 함수형 컴포넌트와 Hooks 테스팅

1. HookCounter.js 와 App.js 작성

<HookCounter.js>
import React, { useState, useCallback } from 'react';

const HookCounter = () => {
  const [number, setNumber] = useState(0);
  const onIncrease = useCallback(() => {
    setNumber(number + 1);
  }, [number]);
  const onDecrease = useCallback(() => {
    setNumber(number - 1);
  }, [number]);

  return (
    <div>
      <h2>{number}</h2>
      <button onClick={onIncrease}>+1</button>
      <button onClick={onDecrease}>-1</button>
    </div>
  );
};

export default HookCounter;
<app.js>
import React from 'react';

import HookCounter from './HookCounter';

function App() {
  return (
    <div>
      <HookCounter />
    </div>
  );
}

export default App;
  1. 함수형 컴포넌트에서는 클래스형 컴포넌트와 달리 인스턴스 메서드 및 상태를 조회할 방법이 없다. 그리고 반드시 shallow가 아닌 mount함수를 사용!!
    -> useEffect는 shallow에서 동작 X. shallow는 재렌더링시키지 않기 위해 사용하는 목적을 가졌지만, useEffect는 컴포넌트를 업데이트(mount)하기 위해 사용되는 hook이기 때문.
<HookCounter.test.js>
import React from 'react';
import { mount } from 'enzyme';
import HookCounter from './HookCounter';

describe('<HookCounter />', () => {
  it('matches snapshot', () => {
    const wrapper = mount(<HookCounter />);
    expect(wrapper).toMatchSnapshot();
  });
  it('increases', () => {
    const wrapper = mount(<HookCounter />);
    let plusButton = wrapper.findWhere(
      node => node.type() === 'button' && node.text() === '+1'
    );
    plusButton.simulate('click');
    plusButton.simulate('click');

    const number = wrapper.find('h2');

    expect(number.text()).toBe('2');
  });
  it('decreases', () => {
    const wrapper = mount(<HookCounter />);
    let decreaseButton = wrapper.findWhere(
      node => node.type() === 'button' && node.text() === '-1'
    );
    decreaseButton.simulate('click');
    decreaseButton.simulate('click');

    const number = wrapper.find('h2');

    expect(number.text()).toBe('-2');
  });
});

참조: https://learn-react-test.vlpt.us/#/02-tdd-introduction

https://velog.io/@citron03/React-18%EC%97%90%EC%84%9C-ReactDOM.render%EC%99%80-createRoot

profile
기록하여 기억하고, 계획하여 실천하자. will be a FE developer (HOME버튼을 클릭하여 Notion으로 놀러오세요!)

0개의 댓글