리액트 테스팅

홍준섭·2023년 7월 3일

react

목록 보기
26/29

테스트란?

우리가 작성한 코드가 잘 작동하는지 검증하는 작업. 그러나 우리가 만든 프로젝트의 모든 기능을 사람이 수동으로 하나하나 확인하는 것은 정말 번거로운 일이다. 그래서 테스트 자동화 작업이 필요하다.

테스트 자동화의 이점

우리가 준비해놓은 상황에 대하여 자동으로 빠르게 검사를 해줄 수 있기 때문에 코드가 이전과 똑같이 작동하는지 아니면 고장났는지 쉽게 판단을 할 수 있어서 써내려가는 코드가 기존의 기능들을 실수로 망가뜨리는것을 효과적을 방지할 수 있다. 또한 개발하게 될 때 실제 발생 할 수 있게 되는 상황에 대하여 미리 정리해놓고 그에 맞춰 코드를 작성하게 되면 실수로 빠뜨릴 수 있는 사항들을 까먹지 않고 잘 챙길 수 있게 된다.
더하여 코드를 리팩토링할때 규모가 커다란 기능이라면 실수로 빠뜨릴 수 있는 사항도 있을 수 있다 그런데 테스트 코드가 있다면 리팩토링 이후에 코드가 이전과 똑같이 작동하는지 검증하는게 매우 쉬워지기 때문에 코드의 질을 향상시키는 것에 매우 큰 도움이 된다.

유닛 테스트

예시)

  • 컴포넌트가 잘 렌더링 된다
  • 컴포넌트의 특정 함수를 실행하면 상태가 우리가 원하는 형태로 바뀐다
  • 리덕스의 액션 생성 함수가 액션 객체를 잘 만들어낸다
  • 리덕스의 리듀서에 상태와 액션객체를 넣어서 호출하면 새로운 상태를 잘 만들어준다.
    프로젝트의 기능을 잘게잘게 쪼개서 테스트를 하면 각 기능이 모두 잘 작동하는지 확인 할 수는 있다. 그런데 전체적으로 잘 작동하는지 확인이 잘 안될 수도 있다.

통합 테스트

예시)

  • 여러 컴포넌트들을 렌더링하고 서로 상호 작용을 잘 하고 있다.
  • DOM 이벤트를 발생 시켰을 때 우리의 UI에 원하는 변화가 잘 발생한다.
  • 리덕스와 연동된 컨테이너 컴포넌트의 DOM에 특정 이벤트를 발생시켰을 때 우리가 원하는 액션이 잘 디스패치된다.
    통합 테스트는 여러 요소들을 고려하여 작성. 유닛 테스트는 보통 한 파일만 불러와서 진행하는 반면, 통합 테스트는 여러 요소들을 고려하는 과정에서, 여러 파일들을 불러와서 사용하게 될 수도 있다. 추가적으로, 한 파일에 있는 여러 기능들을 함께 사용하는 것도 통합테스트로 간주된다.

자바스크립트 테스팅

작업환경 설정

$ npm install --save jest

테스트 코드 작성 예시

//sum.js
function sum(a,b){
  return a+b;
}

module.exports = sum;
//sum.test.js
const sum = require('./sum');
test('1+2 = 3', ()=>{
  expect(sum(1,2)).toBe(3);
});

test함수는 새로운 테스트 케이스를 만드는 함수. 그리고 expect는 특정 값이 ~~ 일 것이다 라고 사전에 정의를 하고, 통과를 하면 테스트를 성공시키고 통과를 하지 않으면 테스트를 실패시킨다. toBe는 matchers라고 부르는 함수인데, 특정 값이 어떤 조건을 만족하는지 또는 어떤 함수가 실행이 됐는지 에러가 났는지 등을 확인 할 수 있게 해준다.

//package.json
{
  "name": "javascript",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "dependencies": {
    "@types/jest": "^24.0.13",
    "jest": "^24.8.0"
  },
  "scripts": {
    "test": "jest --watchAll --verbose"
  }
}
npm test

describe를 사용해서 여러 테스트 케이스를 묶기

//sum.js
function sum(a, b) {
  return a + b;
}

function sumOf(numbers) {
  let result = 0;
  numbers.forEach(n => {
    result += n;
  });
  return result;
}

// 각각 내보내기
exports.sum = sum;
exports.sumOf = sumOf;
//sum.test.js
const { sum, sumOf } = require('./sum');

describe('sum', () => {
  it('calculates 1 + 2', () => {
    expect(sum(1, 2)).toBe(3);
  });

  it('calculates all numbers', () => {
    const array = [1, 2, 3, 4, 5];
    expect(sumOf(array)).toBe(15);
  });
});

TDD

TDD는 테스트가 개발을 이끌어 나가는 형태의 개발론이다.

TDD의 3가지 절차

실패

실패하는 테스트 케이스를 먼저 만들어라. 실패하는 테스트 케이스를 만들 때는 프로젝트의 전체 기능에 대하여 처음부터 모든 테스트 케이스를 작성하는 것이 아니라, 지금 가장 먼저 구현할 기능 하나씩 테스트 케이스를 작성한다.

성공

실패하는 테스트 케이스를 통과시키기 위하여 코드를 작성하여 테스트를 통과시크는 것

리팩토링

구현한 코드에 중복되는 코드가 있거나, 혹은 더 개선시킬 방법이 있다면 리팩토링을 진행한다. 리팩토링을 진행하고 나서도 테스트 케이스가 성공하는지 확인한다. 이 절차가 끝났다면, 다시 첫번째 절차로 돌아가서 다음 기능 구현을 위하여 새로운 실패하는 테스트 케이스를 작성한다.

TDD의 장점

작은 단위로 만들기 때문에, 코드가 너무 방대해지지 않고, 코드의 모듈화가 자연스럽게 잘 이루어지면서 개발이 진행된다.
TDD를 하면 자연스레 테스트 커버리지가 높아질 수 밖에 없다. 그렇기에 리팩토링도 쉬워지고 유지보수도 쉬워진다. 결국 프로젝트의 퀄리티를 높이기에 좋은 환경이 구성된다.
버그에 낭비하는 시간도 최소한으로 할 수 있고 우리가 구현한 기능이 요구사항을 충족하는지 쉽게 확인 할 수 있다.

리액트 컴포넌트의 테스트

리액트 컴포넌트를 테스팅 할 때에는 react-dom/test-utils 안에 들어있는 유틸 함수를 사용해서 테스트 코드를 작성한다 그런데 위 유틸 함수를 직접 사용해서 테스트 코드를 작성하는건 불가능한건 아니지만 조금 복잡하고, 불편한점들이 있기 때문에, 테스팅 라이브러리를 사용 하는것을 리액트 공식문서에서도 권장하고 있다.

react-testing-library 사용법

모든 테스트를 DOM 위주로 진행한다. 그리고, 컴포넌트의 props나 state를 조회하는 일은 없다. 컴포넌트를 리팩토링하게 될 때에는 주로 내부 구조 및 네이밍은 많이 바뀔 수 있어도 실제 작동 방식은 크게 바뀌지 않는다.

설치

npm install --save-dev @testing-library/react

src 디렉토리에 setupTests.js 파일 생성

//src/setupTests.js
import 'react-testing-library/cleanup-after-each';
import 'jest-dom/extend-expect';

테스트 코드

//src/Profule.js
import React from 'react';

const Profile = ({username,name}) =>{
  return (
    <div>
    	<b>{username}</b>&nbsp;
      	<span>({name})</span>
	</div>
    );
};
export default Profile;
//src/App.js
import React from 'react';
import Profile from './Profile';

const App = ()=>{
  return <Profile username = 'hjs' name = 'hjs'/>
};

export default App;
//src/Profile.test.js

import React from 'react';
import {render} from 'react-testing-library';
import Profile from '/Profile';

describe('<Profile/> ', ()=>{
  it('matches snapshot', ()=>{
    const utils = render(<Profile username="hjs" name='hjs'/>);
   	expect(utils.container).toMatchSnapshot();
  });
  it('shows the props correctly', ()=> {
    const utils = render(<Profile username="hjs" name="hjs" />);
    utils.getByText('hjs'); // hjs 라는 텍스트를 가진 엘리먼트가 있는지 확인
    utils.getByText('(hjs)'); // (hjs) 이라는 텍스트를 가진 엘리먼트가 있는지 확인
    utils.getByText(/h/); // 정규식 /h/ 을 통과하는 엘리먼트가 있는지 확인
  });
});

여기서 npm test를 실행하면 작성한 테스트가 잘 통과하는지 확인 할 수 있다.

스냅샷 테스팅

스냅샷 테스팅이란, 렌더링된 결과가 이전에 렌더링한 결과와 일치하는지 확인하는 작업을 의미한다.

다양한 쿼리

render 함수를 실행하고 나면 그 결과물 안에는 다양한 쿼리 함수들이 있는데, 이 쿼리 함수들은 react-testing-library의 기반인 dom-testing-library에서 지원하는 함수들이다.

이 쿼리 함수들은 Variant와 Queries의 조합으로 네이밍이 이루어져 있다.

Variant

  • getBy
    getBy로 시작하는 쿼리는 조건에 일치하는 DOM 엘리먼트 하나를 선택한다. 만약에 없으면 에러가 발생한다
  • getAllBy
    getAllBy로 시작하는 쿼리는 조건에 일치하는 DOM 엘리먼트 여러개를 선택한다. 만약에 하나도 없으면 에러가 발생한다.
  • queryBy
    queryBy로 시작하는 쿼리는 조건에 일치파는 DOM 엘리먼트 하나를 선택한다. 만약에 존재하지 않아도 에러가 발생하지 않는다.
  • queryAllBy
    queryAllBy로 시작하는 쿼리는 조건에 일치하는 DOM 엘리먼트 여러개를 선택한다. 만약에 존재하지 않아도 에러가 발생하지 않는다
  • findBy
    findBy로 시작하는 쿼리는 조건에 일치하는 DOM 엘리먼트 하나가 나타날 때 까지 기다렸다가 해당 DOM을 선택하는 Promise를 반환한다. 기본 timeout인 4500ms 이후에도 나타나지 않으면 에러가 발생한다

Queries

  • ByLabelText
    label이 있는 input의 label 내용으로 input을 선택한다
<label for="username-input">아이디</label>
<input id="username-input" />

const inputNode = getByLabelText('아이디');
  • ByPlaceholderText
    ByPlaceholderText는 placeholder 값으로 input및 textarea를 선택한다
<input placeholder="아이디" />;

const inputNode = getByPlaceholderText('아이디');
  • ByText
    ByText는 엘리먼트가 가지고 있는 텍스트 값으로 DOM을 선택한다
<div>Hello World!</div>;

const div = getByText('Hello World!');

텍스트 값에 정규식을 넣어도 작동한다

const div = getByText(/^Hello/);
  • ByAltText
    ByAltText는 alt 속성을 가지고 있는 엘리먼트를 선택한다
<img src="/awesome.png" alt="awesome image" />;

const imgAwesome = getByAltText('awesomse image');
  • ByTitle
    ByTitle은 title 속성을 가지고 있는 DOM 혹은 title 엘리먼트를 지니고있는 SVG를 선택 할 때 사용한다.
<p>
  <span title="React">리액트</span>는 짱 멋진 라이브러리다.
</p>

<svg>
  <title>Delete</title>
  <g><path/></g>
</svg>

const spanReact = getByTitle('React');
const svgDelete = getByTitle('Delete');
  • ByDisplayValue
    ByDisplayValue는 input,textarea, select가 지니고 있는 현재 값을 가지고 엘리먼트를 선택한다
<input value="text" />;

const input = getByDisplayValue('text');
  • ByRole
    ByRole은 특정 role 값을 지니고 있는 엘리먼트를 선택한다
<span role="button">삭제</span>;

const spanRemove = getByRole('button');
  • ByTestId
    ByTestId는 다른 방법으로 못 선택할때 사용하는 방법인데, 특정 DOM에 직접 test 할 때 사용할 id를 달아서 선택하는 것을 의미한다.
<div data-testid="commondiv">흔한 div</div>;

const commonDiv = getByTestId('commondiv');

권장 쿼리 우선순위

1.getByLabelText
2.getByPlaceholderText
3.getByText
4.getByDisplayValue
5.getByAltText
6.getByTitle
7.getByRole
8.getByTestId

참조

https://learn-react-test.vlpt.us/#/
https://tecoble.techcourse.co.kr/post/2021-10-22-react-testing-library/

profile
개발 공부중입니다

0개의 댓글