회사 코드에 일부 유틸함수 테스트 코드가 있었는데, 거의 테스트 코드가 작성되고 있지 않았고 7개의 파일만 테스트가 돌아가고 있었다.
팀의 사정상 QA 검증이 없이 배포되는 있는 상황에서 테스트 코드가 있는 것이 더 안전한 개발 방식이라는 판단 하에 테스트 코드를 작성하기로 했고, 컴포넌트 테스트를 위해 react-testing-library를 활용하여 테스트 코드를 작성해보기로 했다.
이미 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 문을 추가할 필요가 없어진다.
이후 실제로 사용하는 컴포넌트의 테스트 코드를 작성해 보았다. 그랬더니 아래와 같은 에러가 발생.
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 설정에서 테스트 환경 구성을 도와주는 설정이어서 테스트가 실행되기 전에 각 테스트 파일에 필요한 추가 설정이나 모의 객체를 제공하는 데 사용된다.
위의 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
라고 쓴 부분은 각자 디렉토리에 맞게 수정하여 사용하면 된다.
위와 같은 에러들을 처리한 뒤 test를 실행시켜 보는데 이번엔 라이브러리와 상관 없는 에러가 발생했다.
이러한 문제들은 jest.config.ts 파일에서 설정을 추가하거나 custom util 함수를 만들어서 해결할 수 있다.
// 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을 작성하지 않아도 된다.
// <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를 작성해주지 않아도 된다.