OTTER-LOG

테스트 코드 어떻게 작성하는게 좋을까?

테스트 코드 어떻게 작성하는게 좋을까?
by otter2023년 2월 15일에 최종수정되었습니다.
잘못된 내용이 있으면 댓글을 달아주세요.

테스트 코드를 쓰면서 생각했던 문제

얼마전에 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) 부분은 굉장히 중복적으로 사용이 필요한 부분이었는데 이 부분 또한 함수로 정리해 간편하게 사용할 수 있었습니다.

느낀점

제가 이전에 쓴 테스트 코드를, 제가 보았을 때도 나중에 볼 때 읽기 힘들 것이라는 생각이 들었습니다. 저도 읽기 힘든데, 다른 사람은 전혀 읽을 수 없을 것이라는 생각도 들었습니다. 조금 더 찾아보고 공부해보니 이전의 방법이 일종의 안티패턴처럼 여겨질 수 있다는 것을 알게되었습니다. 😅  테스트 코드도 코드이므로, 가독성 좋게 쓸 수 있는 방법에 대해 더 고민이 필요하다고 느꼈습니다.

Ref