리액트 TDD - 1. 스냅샷, 쿼리

jonyChoiGenius·2023년 2월 21일
0

React.js 치트 시트

목록 보기
13/22

벨로퍼트와 함께하는 리액트 테스팅

테스트 자동화와 TDD

유닛 테스트 : 작은 단위로 테스트함
통합 테스트 : 여러 컴포넌트 들이 서로 상호작용 하면서 잘 하고 있다.
테스트 자동화 : 코드를 수정, 리팩토링 할 때에, 통합 테스트가 통과되는지를 확인하여 버그를 확인. 버그가 발생하는 상황을 테스트 자동화하여 버그 재발을 방지.

TDD - 실패, 성공, 리팩토링의 과정으로 진행된다.
여기서 '실패'는 일단 테스트 코드를 작성하고,
'성공'은 테스트 코드가 통과될 수 있도록 유닛을 수정하고,
리팩토링은 성공된 로직을 깔끔하게 수정하여 적용하는 것을 의미한다.

Jest 예시

yarn add jest
yarn add @types/jest
pacakge.json 작성하고, yarn test로 실행

  1. 먼저 테스트 코드를 작성한다.
    stats.test.js
const stats = require('./stats');

describe('stats', () => {
  it('가장 큰 값을 찾습니다.', () => {
    expect(stats.max([1, 2, 3, 4])).toBe(4);
  });
});

이때 'it' 키워드와 'test' 키워드는 동일한 역할을 한다.
대신 describe를 작성할 때 'it' 키워드는 '무엇을 할 것이다.' 'test'키워드는 '결과가 이러할 것이다.' 라고 작성한다.
특히 아직 작성하지 않은 코드에 대해서는 'it' 키워드를 쓰는 것이 문맥상 올바르다.

stats 파일 안에 max(Array)를 하면 갖아 큰 값을 찾아 반환한다는 테스트 코드이다.
stats.max라는 함수가 없으므로 실패가 된다.
2. max함수를 추가하고 테스트 코드가 성공이 되도록 작성한다.
stats.js

exports.max = numbers => {
  let result = numbers[0];
  
  for (i=0, i < numbers.length, i++) {
  	if (numbers[i] > result) {
      result = n;
  }
    
  return result;
};

간단한 알고리즘을 이용해 구현했다. 테스트가 성공한다.

  1. 리팩토링을 한다.
    for문 보다는 forEach가 좋아보인다.
exports.max = numbers => {
  let result = numbers[0];
  numbers.forEach(n => {
    if (n > result) {
      result = n;
    }
  });
  return result;
};

테스트에 성공한다.......

사실 자바스크립트에는 Math.max( , , ,...,)라는 함수가 있다.

exports.max = numbers => {
  let result = Math.max(...numbers);
};

테스트에 성공한다.

  1. 통합 테스트
    같은 방식으로, min의 테스트 코드를 작성하고, stats에 min함수를 추가하고, avg의 테스트 코드를 작성하고, stats에 avg함수를 추가하고...
describe('stats', () => {
  it('gets maximum value', () => {
    expect(stats.max([1, 2, 3, 4])).toBe(4);
  });
  it('gets minimum value', () => {
    expect(stats.min([1, 2, 3, 4])).toBe(1);
  });
  it('gets average value', () => {
    expect(stats.avg([1, 2, 3, 4, 5])).toBe(3);
  });
});

이런 식으로 stats 모듈의 3개의 유닛에 대한 테스트가 작성된다.

react-testing-library

react-testing-library는 props나 state에 대한 테스트는 진행하지 않는다. dom 단위로, 간결하고 일관된 테스트 코드 작성을 유도한다.

벨로퍼트님 자료와 현재 테스팅 라이브러리가 다르다.
@testing-library로 설치해야 한다.

yarn add @testing-library/react @testing-library/jest-dom @types/jest

설치를 완료하면 src/setupTests.js 파일이 생성된다.

setup

src/setupTests.js

import "@testing-library/jest-dom";
import "@testing-library/jest-dom/extend-expect"; // jest-dom의 확장기능을 불러온다.

@testing-library는 cleanup-after-each가 기본값이다. 이를 원치 않으면 import '@testing-library/react/dont-cleanup-after-each';를 삽입해주면 된다.

컴포넌트 생성

Profile 컴포넌트를 만들어 보자.

@/App.js

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

const App = () => {
  return <Profile username="velopert" name="김민준" />;
};

export default App;

@/Profile.js

import React from "react";

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

export default Profile;

테스트 코드 작성

src/Profile.test.js

import React from "react";
import { render } from "@testing-library/react";
import Profile from "./Profile"; // 컴포넌트를 임포트함

describe("<Profile />", () => {
  it("matches snapshot", () => {
    const utils = render(<Profile username="velopert" name="김민준" />);
    expect(utils.container).toMatchSnapshot();
  });
  it("shows the props correctly", () => {
    const utils = render(<Profile username="velopert" name="김민준" />);
    utils.getByText("velopert"); // velopert 라는 텍스트를 가진 엘리먼트가 있는지 확인
    utils.getByText("(김민준)"); // (김민준) 이라는 텍스트를 가진 엘리먼트가 있는지 확인
    utils.getByText(/김/); // 정규식 /김/ 을 통과하는 엘리먼트가 있는지 확인
  });
});
 PASS  src/Profile.test.js
  <Profile />
    √ matches snapshot (20 ms)
    √ shows the props correctly (12 ms)

 › 1 snapshot written.
Snapshot Summary
 › 1 snapshot written from 1 test suite.

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

Watch Usage: Press w to show more.

테스트를 실행하고 나면 @/__snapshots__ 폴더가 생성되어 있다.

스냅샷 테스팅

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

exports[`<Profile /> matches snapshot 1`] = `
<div>
  <div>
    <b>
      velopert
    </b>
     
    <span>
      (
      김민준
      )
    </span>
  </div>
</div>
`;

위 코드에서 toMatchSnapshot()은 스냅샷 테스트를 위해 사용된다.

유의할 점은 class 명 등도 스냅샷 된다.

import React from "react";
import styled from "styled-components";

const Profile = ({ username, name }) => {
  return (
    <div>
      <b style={{ text: "white" }}>{username}</b>&nbsp;
      <StyledSpan>({name})</StyledSpan>
    </div>
  );
};

export default Profile;

const StyledSpan = styled.span`
  background-color: black;
`;

b태그에는 인라인 스타일을
span태그에는 styled-components를 적용했다.

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

exports[`<Profile /> matches snapshot 1`] = `
<div>
  <div>
    <b>
      velopert
    </b>
     
    <span
      class="sc-bcXHqe hgnsif"
    >
      (
      김민준
      )
    </span>
  </div>
</div>
`;

styled-components에 클래스명이 생성되었다.

쿼리

위 예제에서 getBy로 시작하는 놈들을 쿼리라고 부른다.

자주 쓰이는 쿼리는 아래와 같다.
getByLabelText : 라벨이 특정 텍스트인 인풋 가져오기
getByPlaceholderText : 플레이스 홀더가 특정 텍스트인 인풋 가져오기
getByText : 내용이 특정 텍스트인 요소 가져오기
getByDisplayValue : 'value'어트리뷰트가 특정 값인 요소 가져오기
getByAltText
getByTitle : 'title' 어트리뷰트가 특정 값인 요소 가져오기
getByRole : 'role'어트리뷰트가 특정 값인 요소 가져오기
getByTestId : 'data-testid'가 특정 값인 요소 가져오기

보다시피 getByTestId가 가장 마지막에 있다. 되도록이면 사용을 자제하는 것이 좋다.

counter앱 예시

@/App.js

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

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

export default App;

@/Counter.js

import React, { useState, useCallback } from 'react';

const Counter = () => {
  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 Counter;

@/Counter.test.js

import React from 'react';
import { render, fireEvent } from "@testing-library/react";
import Counter from './Counter';

describe('<Counter />', () => {
  it('matches snapshot', () => {
    const utils = render(<Counter />);
    expect(utils.container).toMatchSnapshot();
  });
  it('숫자가 하나고 버튼이 두개인지', () => {
    const utils = render(<Counter />);
    // 버튼과 숫자가 있는지 확인
    utils.getByText('0');
    utils.getByText('+1');
    utils.getByText('-1');
  });
  
  it('increases', () => {
    const utils = render(<Counter />);
    const number = utils.getByText('0');
    const plusButton = utils.getByText('+1');
    // 클릭 이벤트를 두번 발생시키기
    fireEvent.click(plusButton);
    fireEvent.click(plusButton);
    expect(number).toHaveTextContent('2'); // jest-dom 의 확장 matcher 사용
    expect(number.textContent).toBe('2'); // textContent 를 직접 비교
  });
  
  it('decreases', () => {
    const utils = render(<Counter />);
    const number = utils.getByText('0');
    const plusButton = utils.getByText('-1');
    // 클릭 이벤트를 두번 발생시키기
    fireEvent.click(plusButton);
    fireEvent.click(plusButton);
    expect(number).toHaveTextContent('-2'); // jest-dom 의 확장 matcher 사용
  });
});

fireEvent.이벤트이름(DOM, 이벤트객체);

가령 onChange 이벤트의 경우, 이벤트 객체를 통해 value 값을 넘겨주어야 한다.

fireEvent.change(myInput, { target: { value: 'hello world' } });

해당 이벤트를 발생시킨 후, 요소.toHaveTextContent('기대하는 텍스트') matcher함수에 맞게 myInput.toHaveTextContent('hello world')로 테스트하면 된다.

profile
천재가 되어버린 박제를 아시오?

0개의 댓글