리액트 컴포넌트를 위한 테스트 코드 작성하기

이번에는 리액트 컴포넌트를 위한 테스트 코드를 작성하는 기본 방법에 대하여 알아보겠습니다. 일단, CRA 로 만든 프로젝트에는 이미 App 컴포넌트를 테스트 하는 코드가 존재합니다. src 디렉터리의 App.test.js 를 열어보세요.

App.test.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

it('renders without crashing', () => {
  const div = document.createElement('div');
  ReactDOM.render(<App />, div);
  ReactDOM.unmountComponentAtNode(div);
});

이 테스트 코드는 App 이 오류 없이 제대로 렌더링이 되는지 검증을 해줍니다. 그런데, CRA 로 만든 프로젝트에서는 App 컴포넌트가 초기 상태이고 수정이 되지 않았다면 기본적으로 Jest 에서 이 파일을 무시하게 됩니다.

App.js 를 다음과 같이 수정하고 콘솔상에 뜨는 테스트 결과를 확인해보세요.

App.js

import React from 'react';

const App = () => {
  return <div>App</div>;
};

export default App;
 PASS  src/sample.test.js
 PASS  src/App.test.js

Test Suites: 2 passed, 2 total
Tests:       4 passed, 4 total
Snapshots:   0 total
Time:        0.986s, estimated 1s
Ran all test suites related to changed files.

Watch Usage
 › Press a to run all tests.
 › Press f to run only failed tests.
 › Press q to quit watch mode.
 › Press p to filter by a filename regex pattern.
 › Press t to filter by a test name regex pattern.
 › Press Enter to trigger a test run.

App 을 위한 테스트 코드 실행 결과도 보여지고 있나요? 그런데, 테스트 코드가 두개가 넘어가니까 테스트 코드에 대한 설명이 사라졌습니다. 만약에 테스트 코드에 대한 설명을 보고싶으시다면 터미널 상에서 Ctrl + C 를 눌러서 테스트를 중단한 뒤, 다음 명령어를 통하여 테스트 코드를 검사하세요.

$ yarn test --verbose

그러면, 이렇게 테스트 내용이 상세하게 보여지게 됩니다.

PASS  src/sample.test.js
  sample
    ✓ add (4ms)
    ✓ multiply
    ✓ add & multiply (1ms)

 PASS  src/App.test.js
  ✓ renders without crashing (17ms)

Test Suites: 2 passed, 2 total
Tests:       4 passed, 4 total
Snapshots:   0 total
Time:        1.297s
Ran all test suites related to changed files.

하지만, 코드에서 오류가 나게 된다면 어떤 테스트가 실패했는지 Jest 에서 잘 먕시해 주므로, --verbose 를 무조건 적용하실 필요는 없습니다.

react-test-renderer 사용하기

react-test-renderer 는 페이스북에서 공식적으로 제공하는 테스팅 도구 입니다. 주로, 컴포넌트를 렌더링하고 스냅샷 비교를 할 때 사용하거나, 컴포넌트가 지니고 있는 함수를 직접 호출하고 state 를 검증하는 방식으로 테스트 코드를 작성합니다.

yarn 명령어를 사용하여 라이브러리를 설치해주세요.

$ yarn add react-test-renderer

스냅샷을 비교하기

스냅샷을 비교한다는 것은, 컴포넌트를 렌더링하고 그 결과물을 파일로 저장해둔 다음에, 다음번 테스트를 할 때 이전 결과물과 내용이 일치하는지 확인하는 것 입니다. 이를 통하여 컴포넌트가 렌더링된 HTML 결과물을 비교 할 수 있으니 UI 를 위한 테스팅에서는 꽤나 효과적이죠.

이번에는, 정말 간단한 컴포넌트인 Counter 컴포넌트를 작성해봅시다. src 디렉터리에 Counter.js 파일을 생성하세요.

Counter.js

import React, { Component } from 'react';

class Counter extends Component {
  state = {
    number: 0
  };
  handleIncrease = () => {
    this.setState({
      number: this.state.number + 1
    });
  };
  render() {
    return (
      <div>
        <h1>{this.state.number}</h1>
        <button onClick={this.handleIncrease}>+1</button>
      </div>
    );
  }
}

export default Counter;

그리고 App 에서 렌더링도 해주세요.

App.js

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

const App = ({ array }) => {
  return (
    <div>
      <Counter />
    </div>
  );
};

export default App;

yarn start 를 해서 실제로 잘 작동하는지 직접 확인해보세요.

이제 Counter 컴포넌트의 스냅샷 테스팅을 해주겠습니다. src 디렉터리에 다음 파일을 작성하세요.

Counter.test.js

import React from 'react';
import renderer from 'react-test-renderer';
import Counter from './Counter';

describe('Counter', () => {
  let component = null;

  it('renders corectly', () => {
    component = renderer.create(<Counter />);
  });

  it('matches snapshot', () => {
    const tree = component.toJSON();
    expect(tree).toMatchSnapshot();
  });
});

컴포넌트를 렌더링 할 때는 renderer.create() 함수를 사용하고 이 결과물을 반환 할 때는 component.toJSON() 함수를 사용합니다. toMatchSnapshot() 을 사용하면 결과물을 파일로 저장해주고, 다음번에 비교해줍니다.

코드를 저장하고 나면 테스트가 실행되고 있는 터미널에서 다음과 같은 결과가 나타날 것입니다.

 PASS  src/sample.test.js
 PASS  src/App.test.js
 PASS  src/Counter.test.js
 › 1 snapshot written.

Snapshot Summary
 › 1 snapshot written from 1 test suite.

Test Suites: 3 passed, 3 total
Tests:       6 passed, 6 total
Snapshots:   1 written, 1 total
Time:        2.279s
Ran all test suites related to changed files.

Watch Usage: Press w to show more.

"1 snapshot written." 라는 문구가 나타났지요? src 디렉터리에 __snapshots__ 라는 디렉터리가 생겼을 것입니다. 그 안에 Counter.test.js.snap 을 열어볼까요?

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

exports[`Counter matches snapshot 1`] = `
<div>
  <h1>
    0
  </h1>
  <button
    onClick={[Function]}
  >
    +1
  </button>
</div>
`;

컴포넌트가 렌더링 된 결과물이 문자열로 저장되어있습니다.

한번 Counter 컴포넌트를 조금 수정해볼까요?

Counter.js

import React, { Component } from 'react';

class Counter extends Component {
  state = {
    number: 0
  };
  handleIncrease = () => {
    this.setState({
      number: this.state.number + 1
    });
  };
  render() {
    return (
      <div>
        <h1>값: {this.state.number}</h1>
        <button onClick={this.handleIncrease}>+1</button>
      </div>
    );
  }
}

export default Counter;

h1 태그에 "값" 이라는 텍스트를 추가했습니다. 저장을 하고 나면 스냅샷이 일치하지 않기 때문에 테스트 에러가 나타날 것입니다.

 FAIL  src/Counter.test.js
  ● Counter › matches snapshot

    expect(value).toMatchSnapshot()

    Received value does not match stored snapshot "Counter matches snapshot 1".

    - Snapshot
    + Received

    @@ -1,7 +1,8 @@
      <div>
        <h1>
    +     값: 
          0
        </h1>
        <button
          onClick={[Function]}
        >

      12 |   it('matches snapshot', () => {
      13 |     const tree = component.toJSON();
    > 14 |     expect(tree).toMatchSnapshot();
         |                  ^
      15 |   });
      16 | });
      17 | 

      at Object.toMatchSnapshot (src/Counter.test.js:14:18)

 › 1 snapshot failed.
Snapshot Summary
 › 1 snapshot failed from 1 test suite. Inspect your code changes or press `u` to update them.

만약에 스냅샷을 업데이트 하고 싶으시다면 터미널 창에서 u 키를 누르시면 스냅샷이 업데이트 되고 에러가 해소됩니다.

 PASS  src/sample.test.js
 PASS  src/Counter.test.js
 › 1 snapshot updated.
 PASS  src/App.test.js

Snapshot Summary
 › 1 snapshot updated from 1 test suite.

state 객체와 내장 메서드 접근하기

이번에는 컴포넌트의 내장 메서드를 사용하고, state 객체를 조회하는 방식으로 테스트 코드를 작성해보겠습니다.

먼저, 초기 상태를 잘 지니고 있는지 확인해보겠습니다.

Counter.test.js

import React from 'react';
import renderer from 'react-test-renderer';
import Counter from './Counter';

describe('Counter', () => {
  let component = null;

  it('renders corectly', () => {
    component = renderer.create(<Counter />);
  });

  it('matches snapshot', () => {
    const tree = component.toJSON();
    expect(tree).toMatchSnapshot();
  });

  it('has correct state', () => {
    const instance = component.getInstance();
    expect(instance.state).toEqual({
      number: 0
    });
  });
});

컴포넌트의 인스턴스를 사용 할 때에는 component.getInstance() 함수를 사용합니다. 해당 인스턴스를 사용하여 state 와 메서드들을 사용 할 수 있습니다.

여기서는 toEqual() 이라는 함수를 사용했는데요, toBe() 와의 차이점은 toBe() 는 객체를 비교하게 된다면 정확히 똑같은 객체임을 확인한다면 toEqual() 은 그 내부의 값 하나하나를 비교해서 같은 값을 지니고 있는지 확인합니다. 객체를 확인해야 하는 상황에서는 toEqual() 을 사용하세요.

이번엔, 메서드를 호출해보고 state 를 검증하고 스냅샷도 만들어주겠습니다.

Counter.test.js

import React from 'react';
import renderer from 'react-test-renderer';
import Counter from './Counter';

describe('Counter', () => {
  let component = null;

  it('renders corectly', () => {
    component = renderer.create(<Counter />);
  });

  it('matches snapshot', () => {
    const tree = component.toJSON();
    expect(tree).toMatchSnapshot();
  });

  it('has correct state', () => {
    const instance = component.getInstance();
    expect(instance.state).toEqual({
      number: 0
    });
  });

  it('increases', () => {
    const instance = component.getInstance();
    instance.handleIncrease();

    expect(instance.state.number).toBe(1);

    const tree = component.toJSON();
    expect(tree).toMatchSnapshot();
  });
});

위와 같이 하나의 테스트에서 여러개의 expect 를 사용해도 상관없습니다.

숫자 감소 기능 구현하기

이번에는 숫자를 더하는게 아니라 1씩 감소하는 기능을 구현해봅시다. 이번 차례에는 테스트 코드를 먼저 작성하고 실제 기능을 구현해보겠습니다. 카운터의 테스트 코드에 decrease 라는 테스트를 추가하세요.

Counter.test.js

import React from 'react';
import renderer from 'react-test-renderer';
import Counter from './Counter';

describe('Counter', () => {
  let component = null;

  it('renders corectly', () => {
    component = renderer.create(<Counter />);
  });

  it('matches snapshot', () => {
    const tree = component.toJSON();
    expect(tree).toMatchSnapshot();
  });

  it('has correct state', () => {
    const instance = component.getInstance();
    expect(instance.state).toEqual({
      number: 0
    });
  });

  it('increases', () => {
    const instance = component.getInstance();
    instance.handleIncrease();

    expect(instance.state.number).toBe(1);

    const tree = component.toJSON();
    expect(tree).toMatchSnapshot();
  });

  it('decreases', () => {
    const instance = component.getInstance();
    instance.handleDecrease();

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

    const tree = component.toJSON();
    expect(tree).toMatchSnapshot();
  })
});

increases 를 할 때 숫자를 1로 만들었으니, handleDecrease 라는 함수를 호출하면 숫자가 다시 0으로 바뀌어야 합니다. 테스트 코드를 저장하고 나면 오류가 날 것입니다.

이제 handleIncrease 를 구현하고 버튼도 만들어줘서 해당 메서드를 버튼에 연결시켜줍시다.

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>
        <h1>값: {this.state.number}</h1>
        <button onClick={this.handleIncrease}>+1</button>
        <button onClick={this.handleDecrease}>-1</button>
      </div>
    );
  }
}

export default Counter;

구현은 모두 성공적으로 다 마쳤지만, 스냅샷이 일치하지 않기 때문에 코드를 저장하고 나면 에러가 발생 할 것입니다. 테스트가 실행중인 터미널에서 u 키를 눌러서 스냅샷을 업데이트하세요.

 PASS  src/sample.test.js
 PASS  src/Counter.test.js
 › 2 snapshots updated.
 PASS  src/App.test.js

Snapshot Summary
 › 2 snapshots updated from 1 test suite.

Test Suites: 3 passed, 3 total
Tests:       9 passed, 9 total
Snapshots:   2 updated, 1 passed, 3 total
Time:        0.928s, estimated 1s

테스트 코드가 모두 통과하였나요?

results matching ""

    No results matching ""