jest 테스트코드 작성 / mocking 정리

5o_hyun·2024년 11월 4일
0

테스트코드 구조

✅ given, when, then

describe("회원가입테스트", () => {
	test("테스트케이스작성", () => {
    	//given
      	//when
      	//then
    });
});

한 테스트코드 내에서 given when then으로 나눠서 작성하면 테스트의 가독성과 이해도를 높이고, 명확한 테스트 흐름을 유지하기 좋다.

  • given 준비단계

    • 테스트의 사전 조건이나 초기 상태를 설정
    • 데이터베이스 초기화, 테스트하려는 컴포넌트의 기본 설정
  • when 실행단계

    • 테스트하고자 하는 실제 동작을 실행
    • 특정 메서드를 호출하거나, 컴포넌트를 렌더링하고, 특정 이벤트를 발생
  • then 검증단계

    • 기대하는 결과가 나왔는지 확인
    • 특정 값이 반환되는지, 화면에 특정 요소가 표시되는지

테스트코드 순서

✅ 실패케이스 -> 성공케이스

실패케이스를 먼저적냐, 성공케이스를 먼저적냐 순서에 대해 고민하기 시작했다.
뭐가맞는걸까.....
일단 지금의 내 생각엔 테스트코드를 적는 이유가 오류를 찾아내기 위해서 적는것이 큰 목적이다 보니 우선 실패케이스를 먼저적고, 성공케이스를 나중에 적기로했다.

Jest 실패케이스 작성

위의 구조를 참고하여 먼저 given when then 단계에서 무엇을 할지 정한 후 실패시 테스트케이스를 작성했다.

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import "@testing-library/jest-dom"; // jest로 테스트할때는 테스트파일에 이부분 꼭 import 해야한다.
import { fireEvent, render, screen } from "@testing-library/react";
import SignupPage from "../pages/SignupPage";
import { createMemoryRouter, RouterProvider } from "react-router-dom";

const queryClient = new QueryClient({
  defaultOptions: {},
});

describe("회원가입 테스트", () => {
  test("비밀번호와 비밀번호 확인 값이 일치하지 않으면 에러메세지가 표시된다.", async () => {
    // 1. given 준비단계 - 회원가입 페이지가 그려짐
    // 회원가입이 react-router-dom(useNavigate)나 react-query등을 사용하므로 provider로 감싸줘야한다.
    const routes = [{ path: "/signup", element: <SignupPage /> }];

    const router = createMemoryRouter(routes, {
      initialEntries: ["/signup"],
      initialIndex: 0,
    });

    render(
      <QueryClientProvider client={queryClient}>
        <RouterProvider router={router} />
      </QueryClientProvider>
    );

    // 2. when 실행단계 - 비밀번호롸 비밀번호 확인 값이 일치하지 않음
    // 라벨값으로 input을 가져옴
    const passwordInput = screen.getByLabelText("비밀번호");
    const confirmPasswordInput = screen.getByLabelText("비밀번호 확인");

    // 테스트케이스 실패를 위해, 일부러 password와 wrongPassword에 다른값을 넣어줌
    fireEvent.change(passwordInput, { target: { value: "password" } });
    fireEvent.change(confirmPasswordInput, {
      target: { value: "wrongPassword" },
    });

    // 3. then 검증단계 - 에러메세지가 표시됨
    // id가 error-message인 값을 찾아 존재하는지 확인, 에러메세지는 기다렸다가 실패시 띄워야하기때문에 await
    const errorMessage = await screen.findByTestId("error-message");
    expect(errorMessage).toBeInTheDocument();
  });
});

Jest 성공케이스 작성

test("이메일을 입력하고, 비밀번호와 비밀번호 확인값이 일치하면 회원가입 버튼이 활성화된다", () => {
    // 1. given : 회원가입페이지가 그려짐 위와동일. => beforeEach로 대체
	const routes = [{ path: "/signup", element: <SignupPage /> }];

    const router = createMemoryRouter(routes, {
      initialEntries: ["/signup"],
      initialIndex: 0,
    });

    render(
      <QueryClientProvider client={queryClient}>
        <RouterProvider router={router} />
      </QueryClientProvider>
    );
    // 현재 버튼이 비활성화 되어있는지 테스트
    const signupButton = screen.getByRole("button", { name: "회원가입" });
    expect(signupButton).toBeDisabled();
    // 2. when : 이메일 입력, 비밀번호 비밀번호확인 일치
    const emailInput = screen.getByLabelText("이메일");
    const passwordInput = screen.getByLabelText("비밀번호");
    const confirmPasswordInput = screen.getByLabelText("비밀번호 확인");

    // 테스트케이스 성공을위해, password와 wrongPassword에 같은 값을 넣어줌
    fireEvent.change(emailInput, {
      target: { value: "button-active@gmail.com" },
    });
    fireEvent.change(passwordInput, { target: { value: "password" } });
    fireEvent.change(confirmPasswordInput, {
      target: { value: "password" },
    });

    // 3. then : 회원가입 버튼 활성화
    expect(signupButton).toBeEnabled();
  });

실패케이스와 성공케이스를 따로따로 test로 구현을 하다보니, 처음 동일한 구문을 돌때 똑같은 코드를 또 입력하기가 가독성이 떨어졌다.
이럴때 이론에서 배웠던 beforeEach를 사용하는것이다.
beforeEach는 각각의 test구문을 각각 먼저 실행되는 코드, beforeAll은 모든 test구문 실행 전 한번만 실행되는 코드.
따라서 이런경우는 beforeEach가 실행되어야한다.

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import "@testing-library/jest-dom"; // jest로 테스트할때는 테스트파일에 이부분 꼭 import 해야한다.
import { fireEvent, render, screen } from "@testing-library/react";
import SignupPage from "../pages/SignupPage";
import { createMemoryRouter, RouterProvider } from "react-router-dom";

const queryClient = new QueryClient({
  defaultOptions: {},
});

describe("회원가입 테스트", () => {
  test("비밀번호와 비밀번호 확인 값이 일치하지 않으면 에러메세지가 표시된다.", async () => {
    // 1. given : 회원가입 페이지가 그려짐
    const routes = [{ path: "/signup", element: <SignupPage /> }];

    const router = createMemoryRouter(routes, {
      initialEntries: ["/signup"],
      initialIndex: 0,
    });

    render(
      <QueryClientProvider client={queryClient}>
        <RouterProvider router={router} />
      </QueryClientProvider>
    );

    // 2. when 실행단계 - 비밀번호롸 비밀번호 확인 값이 일치하지 않음
    ...
  });
  
  test("이메일을 입력하고, 비밀번호와 비밀번호 확인값이 일치하면 회원가입 버튼이 활성화된다", () => {
    // 1. given : 회원가입페이지가 그려짐 위와동일.
    // 2. when : 이메일을 입력하고, 
    ...
  });
});
    
    -----------------------------
    위 구문을 아래처럼 변경할 수 있다.
    -----------------------------
      
describe("회원가입 테스트", () => {
  beforeEach(() => {
  	const routes = [{ path: "/signup", element: <SignupPage /> }];

    const router = createMemoryRouter(routes, {
      initialEntries: ["/signup"],
      initialIndex: 0,
    });

    render(
      <QueryClientProvider client={queryClient}>
        <RouterProvider router={router} />
      </QueryClientProvider>
    );
  });
      
  test("비밀번호와 비밀번호 확인 값이 일치하지 않으면 에러메세지가 표시된다.", async () => {
    // 1. given : 회원가입 페이지가 그려짐 => beforeEach로 대체 
    // 2. when 실행단계 - 비밀번호롸 비밀번호 확인 값이 일치하지 않음
    ...
  });
  
  test("이메일을 입력하고, 비밀번호와 비밀번호 확인값이 일치하면 회원가입 버튼이 활성화된다", () => {
    // 1. given : 회원가입페이지가 그려짐 위와동일. => beforeEach로 대체 
    // 2. when : 이메일을 입력하고, 
    ...
  });
});

HTTP request mocking

react-query 활용

지금까지는 api통신이 없는 부분만 구현했지만, 테스트케이스를 적다보니 페이지당의 통합테스트 부분에서 api통신시 에러가나면? 에대한 고찰이 시작됐다.

테스트 케이스 시 에러가 나면 테스트케이스는 성공하나 콘솔에 빨간글씨로 오류처럼 떠서 마치 테스트케이스가 실패한것처럼 보여준다.
react-query 공식문서에서는 아래처럼 터미널에서 꺼주라고 되어있지만, 테스트코드에서 버튼을 클릭하는데, 실제 서버에 요청이 들어간다
https://tanstack.com/query/v4/docs/framework/react/guides/testing

const queryClient = new QueryClient({
  defaultOptions: {},
  // 터미널에서 400에러를 꺼줌 => react-query에서 권장하는방법  : 실제 서버에 요청이 들어감
  logger: {
      log: console.log,
      warn: console.warn,
      error: process.env.NODE_ENV === "test" ? () => {} : console.error,
  },
});

예를들면 로그인을 구현하는데 이메일과 비밀번호를 넣었을때 실제값과 맞는지를 검증하려면 결국 서버에 직접 요청을 해야하는건데 그건 너무 과소비같았다.
처음에는 이메일과 비밀번호를 각 input에적고 로그인버튼클릭 까지 테스트코드에 넣으니 실제 서버로 http통신이 가는것이였다.
그래서 가짜 통신을 만들어주면 좋겠다는 생각을했고 찾아보았다.

✅ nock을 활용

nock이라는 패키지를 설치하면 가짜 request를 보내줄 수 있어, 실제 서버에 요청이 안가 부담이 없다는 장점이 있다.

npm install --save-dev nock 으로 nock을 설치해줬다. 참고사이트 https://github.com/nock/nock

  1. react-query에서 권장했던 방법은 실제 서버에 요청이가기때문에 queryClient부분의 logger를 주석처리한다. => 안쓸거임
  2. react-query에서 권장했던 에러지우는 방법을 안쓸거니 mocking을 이용해 좀 더 확실하게 직관적으로 꺼준다.
beforeEach(() => {
  jest.spyOn(console, "error").mockImplementation(() => {});
});
 afterAll(() => {
  jest.restoreAllMocks();
});
  1. nock의 해당부분을 참고해 가짜 request를 만든다.
nock("https://inflearn.byeongjinkang.com") // 서버에 요청하지않고 nock 패키지로 bad request를 만듬
  .post("/user/login/", {
    username: "wrong@email.com",
    password: "wrongPassword",   
  })
  .reply(400, { id: "NO_SEARCH_USER" });

최종코드

const queryClient = new QueryClient({
  defaultOptions: {},
  // 터미널에서 400에러를 꺼줌 => react-query에서 권장하는방법  https://tanstack.com/query/v4/docs/framework/react/guides/testing
  //   logger: {
  //     log: console.log,
  //     warn: console.warn,
  //     error: process.env.NODE_ENV === "test" ? () => {} : console.error,
  //   },
});

describe("로그인 테스트", () => {
  /*
    mocking을 이용해 400에러를 좀 더 확실하게 직관적으로 꺼줌
    에러있을때는 아무것도 실행하지말아라, 그리고 테스트케이스 다 돌면 원상복구해라

    하지만 이 방법의 문제점은 실제 서버에 request가 들어간다.
    따라서, http request를 mocking하는 방법으로 개선할수있다. 
    이는 서버에서 bad request 온것처럼 구현하는것인데 nock이라는 패키지를 설치해서 구현할 수 있다. '$ npm install --save-dev nock' 참고문서: https://github.com/nock/nock
*/
  beforeEach(() => {
    jest.spyOn(console, "error").mockImplementation(() => {});
  });
  afterAll(() => {
    jest.restoreAllMocks();
  });

  test("로그인에 실패하면 에러메세지가 나타난다.", async () => {
    // given - 로그인 페이지가 그려짐
    const routes = [{ path: "/signup", element: <LoginPage /> }];

    const router = createMemoryRouter(routes, {
      initialEntries: ["/signup"],
      initialIndex: 0,
    });

    render(
      <QueryClientProvider client={queryClient}>
        <RouterProvider router={router} />
      </QueryClientProvider>
    );

    // when - 사용자가 로그인에 실패함
    nock("https://inflearn.byeongjinkang.com") // 서버에 요청하지않고 nock 패키지로 bad request를 만듬
      .post("/user/login/", {
        username: "wrong@email.com",
        password: "wrongPassword",
      })
      .reply(400, { id: "NO_SEARCH_USER" });

    const wrapper = ({ children }) => (
      <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
    );
    const { result } = renderHook(() => useLogin(), { wrapper });

    /* 
        에러메세지 검증할떄 실제 http call이 안되서 테스트케이스 오류가 발생한다. 
        이메일과 비밀번호에 잘못된값을 넣고 로그인버튼을 눌러보면 테스트케이스는 성공했고 400에러가뜬다. 
    */
    const emailInput = screen.getByLabelText("이메일");
    const passwordInput = screen.getByLabelText("비밀번호");
    fireEvent.change(emailInput, { target: { value: "wrong@email.com" } });
    fireEvent.change(passwordInput, { target: { value: "wrongPassword" } });
    const loginButton = screen.getByRole("button", { name: "로그인" });
    fireEvent.click(loginButton);

    // then - 로그인 에러 메세지가 화면에 나타남
    await waitFor(() => result.current.isError);

    // 에러메세지검증
    const errorMessage = await screen.findByTestId("error-message");
    expect(errorMessage).toBeInTheDocument();
  });
});
profile
학생 점심 좀 차려

0개의 댓글