vitest + zustand 조합으로 테스트코드 쓰기

유키미아우·2023년 11월 15일
2

테스트코드는 처음인데 vitest + zustand 관련 자료가 너무 없어..

내 프로젝트는 Vite로 빌드했다. 따라서 config를 공유하는 장점이 있는 vitest를 통해 테스트코드 작성을 시작했다.

초보단계의 문법지식으로 일단 DOM 요소 유무를 시험하는 테스트코드를 뚝딱뚝딱 작성했다.
이윽고 한층 더 복잡한 테스트를 작성하고자 하니 내 프로젝트의 곳곳에 관여하고 있는 상태관리 라이브러리 zustand와 마주해야 했다.

그러나 해외포럼에서는 zustand 테스트 관련 글이 드물고 GPT씨는 jest무새인 시츄에이션..
지푸라기라도 붙잡는 심경으로 Github 레포를 이잡듯 뒤지다가 어떤 브라질인 개발자의 공개 Repo를 정독하고 학습했다. 땡큐감사오브리가도..😭 원하는 내용을 찾지 못해 막막할 때는 비슷한 기술스택의 프로젝트를 찾아서 응용법을 공부하는것도 좋은 학습방법인 것 같다. 물론 레포 주인이 신뢰할만한 개발자라는 가정하에..

vitest + zustand 테스트코드 작성법을 간단한 예제를 통해 기록한다.
덧붙이자면 zustand의 문법만큼이나 너무나 가볍고 편리했다..

간단 테스트 예제

Header 컴포넌트

import usePageStore from "../store/page";

function Header() {
  const { currentPage } = usePageStore(); // zustand의 스토어에서 currentPage state가져오기

  return (
    <div className="flex items-end w-full h-min-[100px] p-5 pt-12">
      <div className="w-full mr-[240px] -z-10 text-center">
        <span className="h-10 text-xl font-medium">
     	  {currentPage} // currentPage state를 표시하기 e.g. "메인페이지"
		</span>
      </div>
    </div>
  );
}

export default Header;

Zustand 스토어

import { create } from "zustand";

const usePageStore = create(set => ({
  currentPage: "메인페이지", // 디폴트값은 "메인페이지"라고 지정.
  setCurrentPage: newPage => set({ currentPage: newPage }), // currentPage의 세터.
}));

export default usePageStore;

Header 테스트

import { render, screen, act } from "@testing-library/react";
import Header from "../../components/Header";

import usePageStore from "../../store/page";
// 👆 스토어 import

const initialState = usePageStore.getState();
// 👆 usePageStore의 모든 초기값을 불러와 initialState에 할당해주었다.

describe("Header component", () => {
  beforeEach(() => {
    usePageStore.setState(initialState); 
    render(<Header />);
  });
  // 👆 전역스코프에서 미리 "캡쳐"해둔 initialState로 스토어를 초기화해주고 있다. 
  // beforeEach내부에 있으므로, 각 테스트 항목들이 시작하기 전에 작동된다.

  it("page name is changed to '테스트페이지이름' well", () => {
  	// 👆 페이지 이름이 잘 바뀌는지 테스트.
    const { setCurrentPage } = usePageStore.getState();
    // 1️⃣ 스토어에서 setCurrentPage를 디스트럭쳐링 할당법으로 가져왔다.

    act(() => {
      setCurrentPage("테스트페이지이름")
    });
    // 2️⃣ setCurrentPage를 이용해서 "테스트페이지이름"으로 currentPage를 갱신.
    // setState는 비동기작업이다. 따라서 리액트가 상태변경을 감지하고 리렌더링을 마칠때까지 대기할 수 있도록 act로 감싸주기.

    const pageName = screen.getByText("테스트페이지이름");
   	// getByText메서드를 이용해 "테스트페이지이름" DOM요소를 스크린에서 탐색

    expect(pageName).toBeInTheDocument();
    // 3️⃣ true를 반환하고 테스트 종료.
  });
  
  // ------- 스토어가 초기화. 즉, currentPage는 다시 default값인 "메인페이지"가 된다.
  
  it("header shows '메인페이지' as default", () => {
    const pageName = screen.getByText("메인페이지");
   	// 1️⃣ getByText메서드를 이용해 "메인페이지" DOM요소를 스크린에서 탐색.

    expect(pageName).toBeInTheDocument();
    // 2️⃣ true를 반환하고 테스트 종료.
  });
});

하기 쉬운 실수 예시

stickerX는 100, stickerY는 200인 스토어가 있다고 가정하자.

it("update stickerX and stickerY well", () => {
  // 👆 stickerX와 stickerY가 잘 바뀌는지 테스트.
  const { stickerX, stickerY, setStickerX, setStickerY } = useEditStore.getState();

  setStickerX(200);
  setStickerY(300);

  expect(stickerX).toBe(200);
  expect(stickerY).toBe(300);
});

위 테스트의 결과는 ... ?
.
.
.
.
.
.
.
실패한다.
stickerX와 stickerY는 여전히 100과 200이라는 메세지와 함께..

아래와 같이 쓰면 제대로 업데이트된 stickerX, stickerY를 불러올 수 있다.

it("update stickerX and stickerY well", () => {
  const { setStickerX, setStickerY } = useEditStore.getState();

  setStickerX(200);
  setStickerY(300);
  
  const { stickerX, stickerY } = useEditStore.getState();
  // 👆 갱신 이후의 stickerX, stickerY를 불러오자

  expect(stickerX).toBe(200);
  expect(stickerY).toBe(300);
});

디스트럭쳐링 외에 다른 문법을 통해서 store의 state와 세터들을 활용할 수도 있다.
아래는 닷노테이션으로 활용하는 모습.

  const editStore = useEditStore.getState();

  editStore.setSomething("new value");

디스트럭쳐링으로 불러오는 값들이 너무 길어질 때에 가독성을 향상시켜줄 것 같다.
본인이 선호하는 스타일로 응용하면 된다.

profile
능동적인 마음

1개의 댓글

comment-user-thumbnail
2024년 2월 26일

소스가 얼마 없어서 고민하고 있었는데 덕분에 잘 봤습니다!! 👍🏻

답글 달기