테스트 코드 어떻게 작성하는게 좋을까?
테스트 코드를 쓰면서 생각했던 문제
얼마전에 ToggleGroup 컴포넌트에 대한 테스트 코드를 작성해본 적이 있습니다. 그 때에는, beforEach
를 적극 사용해 테스트 코드의 중복을 줄이고자 노력했었습니다.
describe("접근성과 키보드 이벤트에 문제가 없어야 한다.", () => { let rendered: RenderResult; let toggle: HTMLElement; let before: HTMLElement; let after: HTMLElement; let mockOnPressedChange = jest.fn(() => {}); beforeEach(() => { mockOnPressedChange.mockClear(); rendered = render( <> <button>Before</button> <Toggle onPressedChange={mockOnPressedChange}>Toggle</Toggle> <button>After</button> </>, ); toggle = rendered.getByRole("button", { name: /toggle/i }); before = rendered.getByRole("button", { name: /before/i }); after = rendered.getByRole("button", { name: /after/i }); // 이 부분이 계속해서 다른 부분에서도 중복됩니다. }); // beforeEach를 사용한 부분
그런데 테스트 코드를 작성하다 보니 beforeEach
를 너무 많이 사용하고 있었고, 점점 복잡해 지는 순간 이 코드를 다른 사람이 읽을 수 있을까? 하는 고민이 들었습니다. 또한 테스트 코드를 작성하는 저도 중복되는 beforeEach
에 힘들었습니다.
함수를 통해 읽기 쉬운 코드 작성하기
그래서 이번에는 토스트의 테스트코드 관련 문서를 공부하고 다음과 같은 방법으로 진행했습니다.
const renderSelect = <T extends string | string[]>(props?: RootProps<T>) => { const onSelectChange = jest.fn(); const utils = render( <Select.Root multiple={props?.multiple} defaultSelected={props?.defaultSelected} defaultOpen={props?.defaultOpen} onSelectChange={onSelectChange} > <Select.Label>Select Label</Select.Label> <Select.Trigger>Trigger Button</Select.Trigger> <Select.Options> <Select.Option value='one'>one</Select.Option> <Select.Option value='two'>two</Select.Option> <Select.Option value='disabled' disabled> disabled </Select.Option> <Select.Option value='three'>three</Select.Option> </Select.Options> </Select.Root>, ); const Trigger = () => result.getByRole("combobox"); const OptionList = () => result.getAllByRole("option"); const Options = () => result.getByRole("listbox"); const Label = () => result.getByText(/select label/i); const clickTrigger = async () => await userEvent.click(Trigger()); const expectSelectedTrue = (El: HTMLElement) => expect(El).toHaveAttribute("aria-selected", "true"); const expectSelectedFalse = (El: HTMLElement) => expect(El).toHaveAttribute("aria-selected", "false"); return { utils, Trigger, OptionList, Options, Label, clickTrigger, expectSelectedTrue, expectSelectedFalse, onSelectChange, }; };
위와 같은 renderSelect
라는 헬퍼 함수를 작성해, render
부분을 정리했고 이를 불러오는 쿼리들도 미리 적용했습니다. 또한 자주 확인할 것 같은 부분이나 이벤트를 계속 발생시켜야 하는 부분은 함수로 정리해 해당 부분의 중복 코드를 줄이고자 했습니다. 그리고 props
를 기존에 작성하는 Select
와 유사하게 받아, 다른 컴포넌트를 작성하지 않고 props
를 통해 매개변수의 변경 사항을 확인해 보고자 했습니다.
위와 같이 정리를 진행하고 보니, 아래 처럼 단순화되고 선언적인 테스트 코드를 작성할 수 있었습니다.
it("옵션을 선택할 때마다, 선택된 value를 매개변수로 가지는 onSelectChange함수가 호출되어야 한다.", async () => { const { OptionList, clickTrigger, onSelectChange, } = renderSelect(); await clickTrigger(); const [OptionOne] = OptionList() await userEvent.click(OptionOne); expect(onSelectChange).toHaveBeenCalledWith("one"); }); // 중간의 한 부분입니다.
개인적으로 이렇게 진행하고 보니, beforeEach
를 계속해서 사용하는 방법 보다 훨씬 읽기 쉽다고 느껴졌고 이를 작성하는 저도 간편하게 적용할 수 있었습니다.
it("defaultSelected를 통해 초기 선택값을 제어할 수 있어야 한다.", async () => { const { OptionList, clickTrigger, expectSelectedTrue } = renderSelect({ defaultSelected: "one", }); // 매개 변수를 받아와서 원하는 상황을 정리할 수 있습니다. await clickTrigger(); const [optionOne] = OptionList(); expectSelectedTrue(optionOne); });
또, 함수의 매개변수를 통해 초기 환경을 원하는 방식으로 쉽게 arrange할 수 있었습니다.
it("single select는 한번에 하나의 옵션만 선택할 수 있다.", async () => { const { OptionList, clickTrigger, expectSelectedTrue, expectSelectedFalse, } = renderSelect(); await clickTrigger(); const [beforeOne] = OptionList(); await userEvent.click(beforeOne); // 옵션을 선택해서 돔에서 사라지게 됨 await clickTrigger(); // 자주 사용하게 되는 이벤트를 함수로 정리 const [midOne, midTwo] = OptionList(); expectSelectedTrue(midOne); expectSelectedFalse(midTwo); await userEvent.click(midTwo); await clickTrigger(); const [afterOne, afterTwo] = OptionList(); expectSelectedTrue(afterTwo); expectSelectedFalse(afterOne); // 자주 사용하게 될 expect를 함수로 정리 });
Select
컴포넌트를 작성하면서 자주 사용하게 되는 expect(El).toHaveAttribute("aria-selected", "true")
같은 부분을 함수로 정리해 중복을 줄이고 보는 사람또한 예측가능하도록 진행할 수 있었습니다.
또한 await userEvent.click(Trigger)
부분은 굉장히 중복적으로 사용이 필요한 부분이었는데 이 부분 또한 함수로 정리해 간편하게 사용할 수 있었습니다.
느낀점
제가 이전에 쓴 테스트 코드를, 제가 보았을 때도 나중에 볼 때 읽기 힘들 것이라는 생각이 들었습니다. 저도 읽기 힘든데, 다른 사람은 전혀 읽을 수 없을 것이라는 생각도 들었습니다. 조금 더 찾아보고 공부해보니 이전의 방법이 일종의 안티패턴처럼 여겨질 수 있다는 것을 알게되었습니다. 😅 테스트 코드도 코드이므로, 가독성 좋게 쓸 수 있는 방법에 대해 더 고민이 필요하다고 느꼈습니다.