티스토리 뷰
처음으로 프론트엔드 테스트 코드를 작성했다.
작업을 하고 리팩토링을 많이 하다 보니 자연스럽게 테스트 코드의 필요성이 느껴졌다.
기존 코드의 기능을 분석해서 핵심기능을 완전히 똑같이 구현했다는 확신이 필요했다.
오늘은 훅에 의존적인 컴포넌트의 통합 테스트코드를 작성하면서 어려웠던 점과 생각보다 쉽게 해결한 경험을 작성한다.
export const Index = ({ options, searchKey, name, width }: ILabeledSelectProps) => {
const { addURLparams, searchParams } = useURLparams();
const urlValue = searchParams.get(searchKey);
const onChange = (value: string) => {
addURLparams(searchKey, value);
};
return (
<div className={LabelSelectStyle}>
<label className={LabelStyle}>{name}</label>
<Select
style={{ width }}
defaultValue={urlValue ?? options[0].value}
onChange={(value: string) => onChange(value)}
options={options}
/>
</div>
);
};
위와 같은 간단한 컴포넌트 테스트부터 시작했다.
label과 select가 랜더링 되고, select값이 변경되면 addURLparams메서드가 실행되며 url search params가 변경된다.
테스트 케이스는
'name props로 전달받은 label과 기본값이 반영된 select가 랜더링 된다.'
'search params가 있으면 해당 값으로 초기화된 select가 랜더링 된다.'
'select를 변경하면 router.replace메서드가 호출된다'
세 가지 중에 'select를 변경하면 router.replace메서드가 호출된다'가 있는 이유는 addURLparams 메서드는 결국 replace메서드를 호출하기 때문이다.
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
export const useURLparams = () => {
const { replace } = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const params = new URLSearchParams(searchParams.toString());
const addURLparams = (key: string, value: string) => {
params.set(key, value);
replace(`${pathname}?${params}`);
};
...
이 컴포넌트를 jsdom환경에서 랜더링 하려면 useURLparmas에 대한 mocking이 필요한데, 그 이유는 내부적으로 'next/navigation'에 의존하고 있기 때문이다.
그렇다면 useURLparams훅을 사용하는 모든 컴포넌트는 next/navigation mocking을 매번 해야 할까?
그것보다는 useURLparams훅에 대한 유닛테스트를 작성해서 모든 메서드가 정확히 동작하는지 검증하고
useURLparams를 가져다 쓰는 컴포넌트에서는 addURLparams, removeURLparams 같은 메서드를 잘 호출하는지만 테스트하면 되겠다. 이렇게 하면 매번 next/navigation부터 훅까지 모두 mocking 하지 않아도 되니까 더 신뢰도 있고 보기 편한 테스트가 된다.
const mockReplace = vi.fn();
const mockPathname = 'http://mysite';
const mockSearchParams = new URLSearchParams();
vi.mock('next/navigation', () => ({
useRouter: () => ({ replace: mockReplace }),
usePathname: () => mockPathname,
useSearchParams: () => ({
toString: () => mockSearchParams.toString(),
get: (key: string) => mockSearchParams.get(key),
has: (key: string) => mockSearchParams.has(key),
}),
}));
describe('useURLparams hook', () => {
test('addURLparams를 호출하면 search params 가 추가된다', () => {
const { result } = renderHook(() => useURLparams());
act(() => {
result.current.addURLparams('status', 'ALL');
});
expect(mockReplace).toHaveBeenCalledWith('http://mysite?status=ALL');
});
test('addURLparams를 동일한 key로 호출하면 search params 가 갱신된다', () => {
...
});
test('removeURLparams를 호출하면 search params가 제거된다', () => {
...
});
test('존재하지 않는 key로 removeURLparams를 호출해도 에러가 발생하지 않는다.', () => {
...
});
test('replaceParamKey를 호출해서 기존에 존재하는 params key를 대체할 수 있다.', () => {
...
});
test('replaceParamKey를 호출할때 oldKey를 잘못 입력해도 에러가 발생하지 않고 무시된다.', () => {
...
});
});
내부적으로 next/navigation의 useRouter, usePathname, useSearchParams만 사용하므로 이것만 모킹 한다.
replace메서드는 mockReplace라는 스파이함수로 모킹 되었다.
useURLparams훅의 addURLparams메서드를 호출했을 때 mockReplace가 내가 원하는 값과 함께 호출되는지만 테스트하면 된다.
이제 useURLparams의 테스트가 완료되었으니 컴포넌트 통합테스트를 작성할 수 있다.
const mockAddURLparams = vi.fn();
const mockSearchParams = { get: vi.fn() };
vi.mock('@/hooks/useURLparams', () => ({
useURLparams: () => ({
addURLparams: mockAddURLparams,
searchParams: mockSearchParams,
}),
}));
describe('SearchController.LabeledSelect 컴포넌트', () => {
const mockOptions = [
...
];
test('name과 기본값이 반영된 select가 랜더링된다.', async () => {
...
});
test('url params가 있으면 해당 값으로 초기화된 select가 랜더링된다.', async () => {
...
});
test('select를 변경하면 addURLparams 함수가 호출된다.', async () => {
const { user } = await render(<LabeledSelect searchKey="status" name="상태" options={mockOptions} />);
const select = screen.getByRole('combobox');
await user.click(select);
const cancelOption = screen.getByText('취소');
await user.click(cancelOption);
expect(mockAddURLparams).toHaveBeenCalledWith('status', 'CANCEL');
});
});
addURLparams메서드가 유닛테스트로 검증되었기 때문에 이 통합테스트에서는 단순히 그 메서드를 호출하는지만 테스트한다.
컴포넌트 랜더, select를 찾아서 클릭하고 '취소' 항목을 클릭했을 때 mockAddURLparams가 지정한 형식으로 호출되었는지 검증한다.
이제 컴포넌트가 내가 원하는 역할을 하고 있다는 것을 검증할 수 있다.
아직 테스트코드는 몇 번 작성해보지 못했고, 공식문서도 다 읽어보지 못해서 어렵다!
스크린에서 요소를 찾는 것조차 익숙하지 않지만 npm run test명령어 만으로 내가 작성했던 함수, 훅, 컴포넌트가 제대로 동작하고 있다는 것을 알 수 있어서 너무 편하고 신기하다. 앞으로 계속 테스트를 작성해 나갈 예정이지만 TDD는 잘 모르겠다.
이번에 팀원들에게 테스트에 대한 생각을 공유하고 테스트를 작성해 나가면 어떨지 제안했고, 긍정적인 반응을 얻었다.
대화를 통해 e2e, 시각적 회귀테스트까지 넓혀가서 결과적으로 단단한 프론트엔드 프로젝트를 만들어보고 싶다.
'Javascript' 카테고리의 다른 글
사칙연산 후위표기식의 계산 알고리즘 자바스크립트로 만들어보기 (1) | 2024.08.24 |
---|---|
트러블슈팅: blob URL과 새 창에서 메모리 누수 방지하기! (2) | 2024.08.19 |
디자인시스템 유지보수 생산성 어떻게 올릴까? 시각적 회귀 테스트 (0) | 2024.05.24 |
이터레이션, 이터러블, 이터레이터, 이터러블이면서 이터레이터인 것 (2) | 2023.10.05 |
JS가 console.log('hello world')를 하는 방법 (0) | 2023.07.30 |