react-testing-library 셋업해보기

배준형·2023년 7월 11일
0

회사 코드에 일부 유틸함수 테스트 코드가 있었는데, 거의 테스트 코드가 작성되고 있지 않았고 7개의 파일만 테스트가 돌아가고 있었다.

팀의 사정상 QA 검증이 없이 배포되는 있는 상황에서 테스트 코드가 있는 것이 더 안전한 개발 방식이라는 판단 하에 테스트 코드를 작성하기로 했고, 컴포넌트 테스트를 위해 react-testing-library를 활용하여 테스트 코드를 작성해보기로 했다.


1. 동작 확인

이미 react-testing-library는 설치 되어있었고, 잘 동작하는지 간단한 컴포넌트를 추가하여 확인해봤다.

import { useState } from 'react';
import { fireEvent, render } from '@testing-library/react';

const MyComponent = () => {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>Increment</button>
    </div>
  );
};

test('should increment count on button click', () => {
  const { getByText } = render(<MyComponent />);

  const countText = getByText('Count: 0');
  const button = getByText('Increment');

  expect(countText).toBeInTheDocument();
  expect(button).toBeInTheDocument();

  fireEvent.click(button);

  expect(countText.textContent).toBe('Count: 1');
});

타입 에러

이렇게 작성 하자마자 expect(HTMLELEMENT).toBeInTheDocument() 에서 에러 발생.

toBeInTheDocument에서 'JestMatchers<HTMLElement>' 형식에
'toBeInTheDocument' 속성이 없습니다.ts(2339)

이 에러는 toBeInTheDocument 함수를 찾을 수 없는 것을 나타내고, toBeInTheDocument@testing-library/jest-dom 패키지에 포함된 함수이므로 해당 패키지를 설치하고 import 해서 해결할 수 있다.


설치

$ npm install --save-dev @testing-library/jest-dom

or

$ yarn add -D @testing-library/jest-dom

이후 테스트 파일 상단에 import '@testing-library/jest-dom'; 로 import해오면 해당 에러를 해결할 수 있다.

import { useState } from 'react';
import { fireEvent, render } from '@testing-library/react';
import '@testing-library/jest-dom';

// ...

jest.config.ts 로 설정해보기

다만, 위 방법대로 하려면 매번 테스트 파일마다 import해야 하는 번거로움이 있다. 이를 해결하려면 jest.config.ts(또는 js)에서 설정을 통해 import를 작성하지 않아도 적용되도록 만들 수 있다.

// jest.config.ts
export default {
  // ...
  setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect'],
  // ...
};

setupFilesAfterEnv는 Jest 설정 파일에 테스트 실행 전에 실행할 파일들의 배열을 지정하는 옵션이다. 이를 통해 @testing-library/jest-dom의 확장 기능을 모든 테스트 파일에서 사용할 수 있도록 지정해준 것이고, 이렇게 하면 각각의 테스트 파일에서 import 문을 추가할 필요가 없어진다.


2. 종속 라이브러리 문제

이후 실제로 사용하는 컴포넌트의 테스트 코드를 작성해 보았다. 그랬더니 아래와 같은 에러가 발생.

Lottie를 사용 중이었는데, Lottie를 사용하면서 fillStyle 속성을 설정할 수 없다는 에러였다. 이 문제는 테스트 환경 세팅 과정에서 canvas와 관련된 코드 때문에 문제가 발생할 수 있고, 이를 해결하기 위해 jsdom 환경에서 canvas를 모방하는 jest-canvas-mock 라이브러리를 설치해 해결할 수 있다.


설치

$ npm install --save-dev jest-canvas-mock

or

$ yarn add -D jest-canvas-mock

jest.config.ts

// jest.config.ts
module.exports = {
  // ...
  setupFiles: ['jest-canvas-mock'],
  // ...
};

setupFiles는 Jest 설정에서 테스트 환경 구성을 도와주는 설정이어서 테스트가 실행되기 전에 각 테스트 파일에 필요한 추가 설정이나 모의 객체를 제공하는 데 사용된다.


2-1) scss..?? import..??

위의 Lottie 문제를 해결한 후에 테스트를 돌려보니 이번엔 scss import 쪽에서 에러가 발생했다.

처음에는 이 에러가 Jest가 scss 파일을 구문 분석하지 못하는 문제이고, 이를 해결하기 위해 jest 설정 파일에서 scss 관련 설정을 추가해야 하는 줄 알았다.
그래서 identity-obj-proxy 라이브러리를 설치해서 해결하려고 했는데,

  • identity-obj-proxy: JavaScript 프록시 객체를 사용하여 CSS 모듈을 모방하는 라이브러리

설치

$ npm install --save-dev identity-obj-proxy

or

$ yarn add -D identity-obj-proxy

설정

// jest.config.ts
module.exports = {
  // ...
  moduleNameMapper: {
    '\\.(scss|css)$': 'identity-obj-proxy',
  },
  // ...
};

똑같은 에러가 발생하면서 해결되지 않았다. 다시 자세히 보니 SASS 파일 내부의 @import 구문 때문에 문제가 생긴 것으로 보였고, 찾아보니 SASS 파일에서는 @import 구문을 사용하여 다른 SASS 파일을 가져 올 수 있으나 Jest 환경에서는 해당 구문을 처리할 방법이 없기 때문에 이런 에러가 발생할 수 있다고 한다.


해결

이를 해결하려면 @import 구문을 사용하지 않는 방법도 있으나 이미 많은 파일에서 사용중이므로 사용하지 않는 것으로 해결하는 것은 어렵다.

여기서는 해당 import를 jest 환경에서 모킹하여 해결할 수 있다.

// <root_dir>\__mocks__\styleMock.js
export default {};
// jest.config.ts
module.exports = {
	// ...
	moduleNameMapper: {
		'@styles/(.*)': '<rootDir>/__mocks__/styleMock.js',
	// ...
};

여기서 @styles 라고 쓴 부분은 각자 디렉토리에 맞게 수정하여 사용하면 된다.


3. 그 외 설정

위와 같은 에러들을 처리한 뒤 test를 실행시켜 보는데 이번엔 라이브러리와 상관 없는 에러가 발생했다.

  • Context API를 사용하는 컴포넌트의 경우 context의 초기 값이 null 이면 예상치 못한 에러 발생
  • useRouter()와 같은 NextJS hook도 마찬가지
  • react-query를 사용중인 경우 queryClient를 찾지 못하는 문제

이러한 문제들은 jest.config.ts 파일에서 설정을 추가하거나 custom util 함수를 만들어서 해결할 수 있다.


3-1) useRouter() mocking하기

// jest.config.ts
module.exports = {
	// ...
	setupFilesAfterEnv: [
	    '<rootDir>/jest.setup.ts',
	// ...
};
// jest.setup.ts
// useRouter Mock
jest.mock('next/router', () => ({
  useRouter: jest.fn().mockReturnValue({
    events: {
      on: jest.fn(),
      off: jest.fn(),
    },
    push: jest.fn(),
    replace: jest.fn(),
    prefetch: jest.fn(),
    query: {},
    pathname: '',
    asPath: '',
  }),
}));

useRouter뿐만 아니라 다른 여러 함수들에 대해 위와 같이 Mocking 처리를 할 수 있다. jest.config.ts 설정에서 jest.setup.ts 파일을 setupFilesAfterEnv 항목에 추가했으므로 테스트 환경이 구성될 때 해당 mocking이 동작하게 되므로 테스트 파일에선 useRouter에 대한 mock을 작성하지 않아도 된다.


3-2) Context API Provider 적용하기

// <rootDir>/test/utils.tsx
import { ReactElement } from 'react';
import { render, RenderOptions } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from 'react-query';

const customRender = (ui: ReactElement, options?: Omit<RenderOptions, 'queries'>) => {
  const queryClient = new QueryClient();
  const Wrapper = ({ children }: { children?: React.ReactNode }) => (
    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  );

  return render(ui, { wrapper: Wrapper, ...options });
};

export * from '@testing-library/react';
export { customRender as render };
// some.test.tsx
import { render } from '@test/utils';

test('', () => {
  const { getByText } = render(<MyComponent />);
  // ...
});

이제 testing-library에서 제공하는 render 대신 customRender를 사용하기 위해 test/utils에서 각 함수들을 import하여 사용하면 된다. 이렇게 되면 미리 지정한 Wrapper render 함수의 인자로 사용되므로 테스트 파일마다 Provider를 작성해주지 않아도 된다.



참조

profile
프론트엔드 개발자 배준형입니다.

0개의 댓글