728x90

프론트엔드에는 단위테스트,유닛테스트,스냅샷테스트, 비쥬얼회귀테스트, E2E테스트등 다양한 테스트가 있습니다.

각각의 테스트가 무엇이고 보편적으로 언제 사용하는지를 포스팅 하겠습니다.

 

단위 테스트

컴퍼넌트 단위별로 테스트하는 방법입니다.

  • 테스트 대상 : 독립적인 모듈 혹은 공용 컴퍼넌트
  • 한계 : 여러 모듈이 조합 됐을 때의 에러를 확인하기 힘듬. 비즈니스 로직에 맞는지는 확인하기 힘듬
  • 보완 방법 : 비즈니스 로직은 통합테스트, UI검증은 스토리북 등을 사용, 간단한 기능은 통합테스트에서 검증
  • 사용 도구 예시 : Jest, Testing-Library, Vitest

컴퍼너트를 테스트할 때 UI등은 결국 시각적으로 확인해야 할 필요가 있고, 결국 함수나 훅이 제대로 동작하는 선에서 사용하는 것이 좋다고 생각한다.

// sum.js
function sum(a, b) {
  return a + b;
}
module.exports = sum;
// sum.test.js
const sum = require('./sum');

test('adds 1 + 2 to equal 3', () => {
  expect(sum(1, 2)).toBe(3);
});

 

통합 테스트

컴퍼넌트가 조합 되었을 때 비즈니스 로직이 올바른지 검사하는 테스트

  • 테스트 대상 : 모듈의 조합 및 컴퍼넌트가 조합되어 비즈니스 로직에 영향을 주는 경우
  • 한계 : 테스팅 도구를 사용하여 확인하기에, 모킹에 의존함(신뢰성 낮아짐), 전체 워크플로우 검증하진 못함
  • 보완 방법 : 모킹을 최대한 줄이고, 전체 워크 플로우 테스트의 경우 E2E 테스트
  • 사용 도구 예시 : Jest, Testing-Library, Vitest
    예시 
  it('아이템 선택 클릭시 value값이 선택되면서 닫히는가', async () => {
    const trigger = await setUpDropOpen();
    const dropdownItems = screen.getAllByTestId('dropdown-item');
    const value = dropdownItems[0].textContent;
    await userEvent.click(dropdownItems[0]);
    expect(trigger).toHaveTextContent(value as string);
    expect(screen.queryByTestId('dropdown-item')).not.toBeInTheDocument();
  });

스냅샷 테스트

컴퍼넌트의 HTML요소를 스냅샷으로 찍어서 이전과 같은지를 검사하는 테스트

  • 테스트 대상 : 컴퍼넌트 UI 요소, 간단한 함수의 경우 스냅샷으로 테스트 가능
  • 한계 : 컴퍼넌트에 오류가 생겨도 휴먼 에러에 의해 인지하지 못할 수 있음
                실제 랜더링이 아니라 스타일 등을 확인하기 힘듬
  • 보완 방법 : 개발 비용이 넉넉하면, Storybook도입
  • 사용 도구 예시 : Jest, Vitest
const { container } = await render(<Header />);

  expect(container).toMatchInlineSnapshot();

 

비쥬얼 회귀 테스트

스냅샷 테스트와 다르게 실제 렌더된 UI 이미지를 저장하고 검증하는 테스트

Github-actions를 통해 UI리뷰까지 피드백 가능

  • 테스트 대상 : 컴퍼넌트 UI 요소, 화면 크기등에 따라 스타일이 자주 변경되는 요
  • 한계 : 대부분 유료 도구
              원인에 대한 추론을 해야 한다.
              실행 기간이 오래 걸린다.
  • 보완 방법 : 개발 비용이 넉넉하면, Storybook도입
  • 사용 도구 예시 : StoryBook, cromatic

 

E2E테스트

실제 앱 구동과 관련된 전체적인 흐름을 검증한다.

FE에서 BE 전반의 로직을 검증함

  • 테스트 대상 : 앱의 전체 로직
  • 한계 : 실행 시간이 
              외부 환경에 영향을 받아 유지 비용이 큼
              영향 범위가 넓어서 디버깅이 오래 걸림
  • 보완 방법 : 개발 완료 시점에 도입하여 전체적 오류 확인만 하기
  • 사용 도구 예시 : Cypress

 

 

참고영상

https://www.inflearn.com/course/%EC%8B%A4%EB%AC%B4%EC%A0%81%EC%9A%A9-%ED%94%84%EB%9F%B0%ED%8A%B8%EC%97%94%EB%93%9C-%ED%85%8C%EC%8A%A4%ED%8A%B8-2%EB%B6%80/dashboard

728x90

(React)컴파운드 패턴으로 드롭다운 컴퍼넌트 만들기-2 테스트하기

 

 

컴파운드 패턴을 통해서 드롭다운을 만들었고 이를 바탕으로 Select를 만들었습니다.

import useDropDown from '../shared/dropdown/hook/useDropDown';
import { DropDown_Data } from '../constant/data';
import { DropDown } from '../shared/dropdown';

const Select = () => {
  const [isOpen, toggleOpen, value, selectValue] = useDropDown();

  return (
    <DropDown isOpen={isOpen} toggleOpen={toggleOpen}>
      <DropDown.Trigger isOpen={isOpen} toggleOpen={toggleOpen} value={value} />
      <DropDown.Menu
        isOpen={isOpen}
        value={value}
        setValue={selectValue}
        list={DropDown_Data}
        toggleOpen={toggleOpen}
      >
        {DropDown_Data.map((item) => {
          return (
            <DropDown.Item
              key={item.id}
              value={item.value}
              selected={item.value === value}
              toggleOpen={toggleOpen}
              setValue={selectValue}
            ></DropDown.Item>
          );
        })}
      </DropDown.Menu>
    </DropDown>
  );
};

export default Select;

 

이제 해당 컴퍼넌트를 기반으로 테스트코드를 작성하려고 합니다.

https://www.samdawson.dev/article/how-to-test-compound-components/

 

How to test Compound Components

For a quick overview on Compound Compounds check out this article. Compound components are made up of many component parts. The most…

www.samdawson.dev

 

이때 DropDown은 결국 하나의 완성된 select를 만들기 위한 합성 컴포넌트이고, '컴파운드 패턴의 컴퍼넌트가 뭉쳐서 제대로 작동하는지를 확인하려면 전체를 한번에 테스트하는 것이 좋다' 라고 얘기합니다.

 

따라서 저도 Select에서 하나의 테스트로서 해당 컴퍼넌트를 테스트 하기로 했습니다.

 

드랍다운의 폴더구성은 아래와 같습니다.

┣ 📂hook
┃ ┣ 📜useDropClick.ts
┃ ┣ 📜useDropDown.ts
┃ ┗ 📜useDropKeyboard.ts
┣ 📜DropDown.css
┗ 📜index.tsx   

 

각각 클릭이벤트, 상태, 키보드 이벤트를 관리하는 훅 3개와 CSS, 컴퍼넌트를 관리하는 index 파일이 존재합니다.

이 중에서 테스트할 내용은 아래와 같다

  • 랜더링이 제대로 되는지
  • 드랍다운이 제대로 열리는지
  • 키보드 이벤트가 제대로 먹히는 지
  • 외부 영역 클릭시 닫히는 지
  • 아이템을 선택했을 때, 해당 값이 선택되는지

RTL 공식문서에 따르면, 테스트를 할 경우 유저가 사용하는 역할을 통해서 테스트 하는 것이 좋다고 했습니다

이를 위해서 Trigger를 일반 div에서 버튼으로 변경했습니다.

const Trigger: React.FC<TriggerProps> = ({ isOpen, toggleOpen, value }) => {
  return (
    <button
      className={`trigger text ${isOpen ? 'open' : ''}`}
      onClick={toggleOpen}
    >
      {value ? value : '선택해주세요'}
    </button>
  );
};

 

그리고 item의 유무를 통해서 select가 제대로 열렸는지 판단을 하려하는데 아이템에 value값은 동적이기에 data-testid를 부여하였습니다.

const Item: React.FC<ItemProps> = ({
  value,
  setValue,
  toggleOpen,
  selected,
}) => {
  const onClickValue = () => {
    setValue(value);
    toggleOpen();
  };
  return (
    <div
      data-testid="dropdown-item"
      className={`text item ${selected ? 'current' : ' '}`}
      onClick={onClickValue}
    >
      {value}
    </div>
  );
};

 

테스트 코드 작성

반복 작업 모듈화 이전..

import userEvent from '@testing-library/user-event';
import Select from './Select';
import { render, screen } from '@testing-library/react';

it('셀렉트가 초기에 "선택해주세요"로 렌더링 되고, dropdowm이 닫혔는가', () => {
  render(<Select />);

  expect(screen.getByText('선택해주세요')).toBeInTheDocument();
  expect(screen.queryByTestId('dropdown-item')).not.toBeInTheDocument();
});

it('클릭 시 열리면서 item들이 보이는가', async () => {
  render(<Select />);
  const trigger = screen.getByRole('button');
  await userEvent.click(trigger);
  expect(screen.getAllByTestId('dropdown-item')[0]).toBeInTheDocument();
});

it('아이템 선택 클릭시 value값이 선택되면서 닫히는가', async () => {
  render(<Select />);
  const trigger = screen.getByRole('button');
  await userEvent.click(trigger);
  const dropdownItems = screen.getAllByTestId('dropdown-item');
  const value = dropdownItems[0].textContent;
  await userEvent.click(dropdownItems[0]);
  expect(trigger).toHaveTextContent(value as string);
  expect(screen.queryByTestId('dropdown-item')).not.toBeInTheDocument();
});

describe('키보드 이벤트', () => {
  it('열린 후 키보드로 벨류 값이 조작되는가?', async () => {
    render(<Select />);
    const trigger = screen.getByRole('button');
    await userEvent.click(trigger);
    const dropdownItems = screen.getAllByTestId('dropdown-item');
    const value = dropdownItems[0].textContent;
    await userEvent.keyboard('{arrowdown}');
    expect(trigger).toHaveTextContent(value as string);
    expect(dropdownItems[0]).toHaveClass('current');

    await userEvent.keyboard('{arrowdown}');
    expect(trigger).toHaveTextContent(dropdownItems[1].textContent as string);
    expect(dropdownItems[1]).toHaveClass('current');

    await userEvent.keyboard('{arrowup}');
    expect(trigger).toHaveTextContent(value as string);
    expect(dropdownItems[0]).toHaveClass('current');
  });

  it('열렸을 때, enter 혹은 esc로 닫히는가?', async () => {
    render(<Select />);
    const trigger = screen.getByRole('button');
    await userEvent.click(trigger);

    await userEvent.keyboard('{enter}');
    expect(screen.queryByTestId('dropdown-item')).not.toBeInTheDocument();

    await userEvent.click(trigger);
    const dropdownItems = screen.getAllByTestId('dropdown-item');
    expect(dropdownItems.length).toBeGreaterThan(0);

    await userEvent.keyboard('{escape}');
    expect(screen.queryByTestId('dropdown-item')).not.toBeInTheDocument();
  });
});

it('열린 셀렉트가 외부 영역 클릭시 닫히는가?', async () => {
  render(<Select />);
  const trigger = screen.getByRole('button');
  await userEvent.click(trigger);

  // Select 컴포넌트 외부 영역을 클릭합니다.
  const outsideArea = document.body;
  await userEvent.click(outsideArea);
  expect(screen.queryByTestId('dropdown-item')).not.toBeInTheDocument();
});

 

각각을 설명하면 아래와 같습니다.

it('셀렉트가 초기에 "선택해주세요"로 렌더링 되고, dropdowm이 닫혔는가', () => {
  render(<Select />);

  expect(screen.getByText('선택해주세요')).toBeInTheDocument();
  expect(screen.queryByTestId('dropdown-item')).not.toBeInTheDocument();
});

 

trigger를 정의할 때, 값이 undefined면 선택해주세요 라는 값이 보이도록 하였습니다. 따라서 초기에 정상적으로 렌더링이 되었다면 선택해주세요가 보일 것이고, 드롭다운이 닫혔다면 isOpen이 false이기에 item들이 랜더링되지 않습니다.

 

it('클릭 시 열리면서 item들이 보이는가', async () => {
  render(<Select />);
  const trigger = screen.getByRole('button');
  await userEvent.click(trigger);
  expect(screen.getAllByTestId('dropdown-item')[0]).toBeInTheDocument();
    // 윗 줄은 아래 두 줄로 대체가능
	//const dropdownItems = screen.getAllByTestId('dropdown-item');
    //expect(dropdownItems.length).toBeGreaterThan(0);
});

 

위 코드가  trigger를 버튼으로 바꾼 이유입니다. trigger의 text는 선택된 value에 따라 변경되지만 역할은 button으로 고정됩니다. 또한 dropdown에서 유일하게 존재하는 버튼 요소이기엔 탐색하기도 쉽습니다.

user가 클릭했는지를 테스트하고, dropdown아이템이  존재하는지 찾아야 합니다.

이때 유의할 점은 2가지입니다.

  • userEvent는 비동기이기에 async await을 사용해야 한다.
  • 요소가 2개 이상일때는 getByTestId는 에러를 뱉는다.
  • getAllByTestId는 배열이기에 검증시에는 인덱스를 붙여 사용해야 한다.

 

사실 처음에 작성할 때는 queryAllByTestId를 사용해야 하는가 생각했습니다. 하지만 eslint 에러가 떴습니다.

아래 내용을 요약하자면 있다는 사실을 검증할 때는 getBy, 없다는 사실을 검증하려면 queryBy를 사용해야 합니다.

https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/prefer-presence-queries.md

 

eslint-plugin-testing-library/docs/rules/prefer-presence-queries.md at main · testing-library/eslint-plugin-testing-library

ESLint plugin to follow best practices and anticipate common mistakes when writing tests with Testing Library - testing-library/eslint-plugin-testing-library

github.com

 

it('아이템 선택 클릭시 value값이 선택되면서 닫히는가', async () => {
  render(<Select />);
  const trigger = screen.getByRole('button');
  await userEvent.click(trigger);
  const dropdownItems = screen.getAllByTestId('dropdown-item');
  const value = dropdownItems[0].textContent;
  await userEvent.click(dropdownItems[0]);
  expect(trigger).toHaveTextContent(value as string);
  expect(screen.queryByTestId('dropdown-item')).not.toBeInTheDocument();
});

 

이번에도 select를 연 후에, dropDownItems들을 찾아낸 후에 첫 번쨰 값의 text내용을 value로 저장한다.

그리고 첫 번쨰 item을 클릭했을 때, trigger값이 value와 일치하는지 비교한다.

그리고 클릭 후에 닫혔는지를 확인한다.

 

이때  클릭 하기 위해서 trigger를 클릭해서 여는 작업이 매번 반복된다. 하지만 이 내용은 추후에 할 테스트에서는 중요한 부분이 아니고, 가독성만 방해한다.

따라서 이를 모듈화할 필요성이 있다.

 

초기에 생각한 것은 beforeEach 메서드를 사용하는 것이다.

beforeEach(() => {
  render(<Select />);
});

하지만 이렇게 할 경우 eslint에러가 뜬다.

 

해당 링크는 deprecated 되서 https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/no-render-in-lifecycle.md에서 내용을 확인할 수 있는데,  관련 내용은 굳이 beforeEach를 써서 구현해야 하는 내용인가이다.

차라리 모듈화를 통해서 하는 것이 맞다고 해당 내용에서 제시한다.  feforEach와 describe등에 너무 많은 내용들이 담기게 되면, 특정 변수가 어디서 왔는지 찾기도 힘들고, 이를 유지보수하는 사람 입장에서 오히려 불편해진다.

 

따라서  Drop을 연 상태로 세팅하기위 아래와 같이 명명했고, trigger 버튼을 사용할 때도 있기에 return해주었다.

const setUpDropOpen = async () => {
  render(<Select />);
  const trigger = screen.getByRole('button');
  await userEvent.click(trigger);
  return trigger;
};

 

이를 반영해서 테스트 코드들을 수정하였다. 단지 코드만 수정했을 뿐만 아니라, describe를 통해서 좀 더 테스트 내용을 가독성 좋게 바꿀 수 있었다. trigger라는것이  드롭다운을 연 결과물에서 나오는 것도  test내부에서  파악할 수 있다. 

 

모듈화 이후의 코드

it('셀렉트가 초기에 "선택해주세요"로 렌더링 되고, 드롭다운이 닫혔는가', () => {
  render(<Select />);

  expect(screen.getByText('선택해주세요')).toBeInTheDocument();
  expect(screen.queryByTestId('dropdown-item')).not.toBeInTheDocument();
});

it('클릭 시 열리면서 item들이 보이는가', async () => {
  await setUpDropOpen();
  expect(screen.getAllByTestId('dropdown-item')[0]).toBeInTheDocument();
});

describe('셀렉트가 열린 상태에서', () => {
  it('아이템 선택 클릭시 value값이 선택되면서 닫히는가', async () => {
    const trigger = await setUpDropOpen();
    const dropdownItems = screen.getAllByTestId('dropdown-item');
    const value = dropdownItems[0].textContent;
    await userEvent.click(dropdownItems[0]);
    expect(trigger).toHaveTextContent(value as string);
    expect(screen.queryByTestId('dropdown-item')).not.toBeInTheDocument();
  });

  describe('키보드 이벤트가 작동하는가?', () => {
    it('열린 후 키보드로 벨류 값이 조작되는가?', async () => {
      const trigger = await setUpDropOpen();

      const dropdownItems = screen.getAllByTestId('dropdown-item');
      const value = dropdownItems[0].textContent;
      await userEvent.keyboard('{arrowdown}');
      expect(trigger).toHaveTextContent(value as string);
      expect(dropdownItems[0]).toHaveClass('current');

      await userEvent.keyboard('{arrowdown}');
      expect(trigger).toHaveTextContent(dropdownItems[1].textContent as string);
      expect(dropdownItems[1]).toHaveClass('current');

      await userEvent.keyboard('{arrowup}');
      expect(trigger).toHaveTextContent(value as string);
      expect(dropdownItems[0]).toHaveClass('current');
    });

    it('열렸을 때, enter 혹은 esc로 닫히는가?', async () => {
      const trigger = await setUpDropOpen();

      await userEvent.keyboard('{enter}');
      expect(screen.queryByTestId('dropdown-item')).not.toBeInTheDocument();

      await userEvent.click(trigger);
      const dropdownItems = screen.getAllByTestId('dropdown-item');
      expect(dropdownItems.length).toBeGreaterThan(0);

      await userEvent.keyboard('{escape}');
      expect(screen.queryByTestId('dropdown-item')).not.toBeInTheDocument();
    });
  });

  it('외부 영역 클릭시 닫히는가?', async () => {
    await setUpDropOpen();

    // Select 컴포넌트 외부 영역을 클릭합니다.
    const outsideArea = document.body;
    await userEvent.click(outsideArea);
    expect(screen.queryByTestId('dropdown-item')).not.toBeInTheDocument();
  });
});

 

 

피드백

위 코드는 UI 랜더링에 관련된 코드고 별도의 API로직 등에는 관련이 없다. 비동기적인 요소도 없고,,

StoryBook을 통한 비쥬얼 테스트로 끝내는게 가장 간단하고 적합하지 않았을까? 생각이 든다.

 

'프론트엔드 > 테스트' 카테고리의 다른 글

프론트엔드의 테스트 종류  (0) 2024.04.06
React-Testing-Library 테스트 작성하기  (0) 2024.03.31
728x90

시작하며..

프로젝트를 진행하면서 코드 리뷰를 하다보면 어떤 의도로 해당 코드를 작성했는지 직접 확인 혹은 설명을 들어야 하기에 많은 시간을 잡아먹게 된다. 또한 GitAction을 통해 CI/CD 파이프라인을 만들었다 생각했지만, 사실 CI단계에서 빌드에러만 검증하고 있었다.

이러한 점을 테스트코드를 작성하게 되면 해결할 수 있지만 '어떤 그리고 어떻게' 작성 해야하는지가 막막했었다. 이를 정리해보려고 한다. 그 중에서 단위테스트통합 테스트에 사용할 수 있는 내용을 정리하려고 한다.

설명 내용은 RTL (React testing-library)에 관한 것이다.

테스트코드의 장점과 작성원칙

테스트란 앱의 품질 안정성 높이기 위해 사전에 결함을 찾아내 수정하기 위한 일련의 행위다.

 

테스트 코드를 쓰게 되면 얻게 되는 장점은 3가지가 있다.

1. 리팩토링 작업에 도움이 된다.

2. 잘 쓰여진 테스트코드는 그 자체로 문서의 역할을 한다.

3. 좋은 설계에 대한 고민을 하게 된다. 

 

이러한 역할을 수행하는 테스트코드는 보통 AAA(Arrange Act Assert)원칙에 의해서 작성이 된다.(Given when then)

  • Arrange : 흔히 말하는 Given이며 어떤 상황인지를 말한다( ex : 어떤 컴포넌트에서, 로그인이 된 상황에서~~)
  • Act : when이며 테스트할 동작을 말한다 ( ex :버튼을 눌렀을 때)
  • Assert : then이며 어떤 동작이 일어나는지를 검증한다 (  ex : 값을 증가한다, 무슨 함수가 호출된다.)
// App.test.jsx
import { render, screen } from '@testing-library/react';
import App from './App';

test('renders learn react link', () => {
  render(<App />);
  const linkElement = screen.getByText(/learn react/i);
  expect(linkElement).toBeInTheDocument();
});

 

테스트 코드를 써본적이 없더라도, CRA를 통해 프로젝트를 생성하면 위와 같은 파일이 생성된 것을 확인할 수 있다.

위의 코드를 해석하면 test할 상황에 대한 설명은 learn react link를 렌더하는가이고,  

App을 렌더링한 상황에서( Arrange)

대소문자를 무시하고 leran react란 단어가 document에 존재할 것이다! (Assert)

 

이 경우에는 렌더링에 대한 검증이기에 Act가 생략이 되어있다.

그리고 이러한 검증은 항상 유저의 입장에서 앱을 이용했을 때를 고려해서 작성해야 한다.

 

위의 예시를 보면 알 수 있듯이 테스트 코드는 특정 요소를 렌더링 된 화면에서 찾아서 그것의 상태를 확인한다.

찾는 것을 쿼리(Queries) , 그리고 이런 것을 확인하는 것을  매쳐(matchers)라고 한다.

 

쿼리(Queries)

출처 : https://testing-library.com/docs/queries/about/#types-of-queries

 

쿼리의 경우 시작하는 부분을 통해서 목적이 구별된다.

위의 표를 통해 각각의 목적을 정리하면 아래와 같다. 

  • getBy : 해당 요소를 얻을 때 사용함
  • queryBy : 해당 요소의 존재 유무를 확인할 때 사용함 ( 없을 때, 에러 대신 null이 뜸))
  • findBy : 데이터를 요구하는 비동기 작업을 테스트할 때 사용한다 retry를 통해 비동기적 요소들을 찾음
  • All : 해당 쿼리문의 탐색 결과가 여러 개인 경우 사용함

 

즉 위의 예시에서는 learn react가 하나로 된 문자열이기에 getByText를 사용한 것이다.(getBy+ Text)

 

쿼리의 끝 부분은 찾을 대상의 종류를 정의하는데 사용된다.

테스트 코드는 유저의 입장에서  검증해야 한다. 따라서 우리가 특정 컴포넌트를 찾을때도 유저입장에서 찾을 수 있는 키워드를 사용하는 것이 선호된다.

이를 공식문서에서는 우선순위를 3단계를 두어 분류 한다.

 

1순위 : 누구나 접근 가능한 요소들

유저는 웹페이지에 접속했을 때, 클래스 네임이나 id등을 알지는 못한다. 보통 '버튼이냐','무슨 글자가 적혔나?' 등으로 구별하는데 이와 관련된 요소들이다.

  1. getByRole:  가장 선호되는 방식으로 대상의 역할로 구분하고, name 을 사용해서 이름을 확인할 수 있다.  ex) getByRole('button', {name: /submit/i})
    역할의 종류는 https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques#roles에서 확인 가능하다.

  2. getByLabelText:  웹사이트 양식을 탐색할 때 사용자는 라벨 텍스트를 사용하여 요소를 찾기 때문에, 이를 이용
  3. getByPlaceholderText:  PlaceHolder로 찾기
  4. getByText: 양식 외부에서는 텍스트 콘텐츠가 사용자가 요소를 찾는 주요 방법.
  5. getByDisplayValue: 양식 요소의 현재 값은 값이 채워진 페이지를 탐색할 때 유용함.

2순위 : 시맨틱 쿼리

일반적으로 혹은 항상 유저가 보는 요소들은 아니지만 대안으로 사용 가능

  1. getByAltText: 이미지등의 Alt를 사용하는 요소들에 사용 가능.
  2. getByTitle: 기본 유저들이나 screenReader에는 읽히지 않지만 사용가능

3순위 : data-testId 부여

getByTestId : 탐색이 불가능할 때 사용가능하며, 컴퍼넌트에 data-testid를 부여해서 탐색한다.

<Grid
      item
      xs={6}
      sm={6}
      md={3}
      onClick={handleClickItem}
      data-testid="product-card"
    >
  await screen.findAllByTestId('product-card');

 

 

매쳐(matchers)

해당 요소를 찾은 다음에 우리가 원하는 결과대로 되었는지 단언할 필요가 있다.

이때 사용하는 것이 Matcher이다. (클래스를 가지게 되었는가(toHaveClass), 모양이 바꼈는가(toHaveStyle))
RTL의 매쳐의 종류는 아래와 같다.
https://github.com/testing-library/jest-dom?tab=readme-ov-file

 

GitHub - testing-library/jest-dom: :owl: Custom jest matchers to test the state of the DOM

:owl: Custom jest matchers to test the state of the DOM - testing-library/jest-dom

github.com

 

 

Act 하는법

이제 쿼리문으로 대상을 찾고 매쳐로 이를 검증할 수 있는데, 우리의 테스트는 유저와의 상호 작용을 기준으로 검증해야 한다.

이때 사용하는 것이  userEvent이다. userEvent와 fireEvent는 클릭등의 이벤트를 직접 일으킬 수 있는 것은 동일하나 fireEvent와 달리 userEvent는 유저 입장에서 클릭을 일으킨다.

예를 들어 버튼을 누를 때, 유저라면 마우스를 버튼에 올리고(호버) 클릭을 한다.

하지만 fireEvent를 쓰면 이 과정없이 클릭이 된다. 

따라서 테스트는 유저 입장에서 검증이 목적이기에 userEvent를 사용하는 것이 더 적합하다.

출처 : https://kentcdodds.com/blog/common-mistakes-with-react-testing-library#using-query-variants-for-anything-except-checking-for-non-existence

 

 

반복 작업 관리 및 테스트 독립성 유지하기

매 테스트마다 반복되는 작업들을 공통적으로 처리할 필요성이 있다.

이때 매 테스트전에 사용되는 것을 setUp, 후에 사용하는 것을 tearDown이라고 한다.

setUp :beforeEach, beforeAll

tearDown : afterEach, afterAll

아래에서는 vi라는 Vitetest의 모킹 도구로 테스트의 독립성을 유지하기 위해서 각각의 테스트가 끝난후에 모킹된 정보를 초기화 하고 있다.

afterEach(() => {
  vi.clearAllMocks();
});

afterAll(() => {
  vi.resetAllMocks();
});

 

함수 모킹하기

우리는 테스트할 때, 특정 함수들을 호출하게 되는 경우가 있다. 특히 외부 모듈에 의한 함수를 호출할 때, 인기 모듈들은 일반적으로 테스트를 거친 것이고 자체적인 테스트를 거친 후 배포된다. 이를 사용할 때, 굳이 해당 모듈에 대한 검증을 진행할 필요는 없다.

또한 특정 함수가 검증되었다면, 굳이 해당 함수를 이용하는 것이 아니라 결과가 해당 함수와 동일한 가상의 함수를 사용해도 충분하다.

이런 경우에 모킹을 하게 된다. 사용법은 아래 게시글을 참고하면 좋을 것 같다.

https://www.daleseo.com/jest-fn-spy-on/

 

Jest의 jest.fn(), jest.spyOn()를 이용한 함수 모킹

Engineering Blog by Dale Seo

www.daleseo.com

 

 

참고 문헌 및 강의

테스트 코드를 잘 쓴다는 개념이 막연해서 inflearn 강의를 들었고 많은 도움을 받았다. NHN테스트코드 강의를 봤었지만, 실사용에서 어려움을 겪고 구매를 했는데 만족스러웠다.

https://www.inflearn.com/course/%EC%8B%A4%EB%AC%B4%EC%A0%81%EC%9A%A9-%ED%94%84%EB%9F%B0%ED%8A%B8%EC%97%94%EB%93%9C-%ED%85%8C%EC%8A%A4%ED%8A%B8-1%EB%B6%80/dashboard

 

실무에 바로 적용하는 프런트엔드 테스트 - 1부. 테스트 기초: 단위・통합 테스트 | 코드 조커, 오

코드 조커, 오프 | 이 강의를 통해 전반적인 프런트엔드 테스트 종류를 파악하고, 상황에 맞는 적절한 테스트 선택을 통해 신뢰감 있는 테스트를 작성하는 방법을 배웁니다., 🎊 이벤트 🎊  1부

www.inflearn.com

 

https://testing-library.com/docs/react-testing-library/setup

 

Setup | Testing Library

React Testing Library does not require any configuration to be used. However,

testing-library.com

 

 

+ Recent posts