가수면

테스팅 라이브러리 심화 본문

React/테스팅 라이브러리

테스팅 라이브러리 심화

니비앙 2023. 6. 26. 09:27

waitFor

비동기에 대한 요청이 전부 처리될 때까지 기다리도록 하는 메소드

function waitFor<T>(
  callback: () => T | Promise<T>,
  options?: {
    container?: HTMLElement
    timeout?: number
    interval?: number
    onTimeout?: (error: Error) => Error
    mutationObserverOptions?: MutationObserverInit
  },
): Promise<T>

예시) 2개의 요청 결과를 기다려야 함

test("스쿱 및 토핑 라우트를 핸들링", async () => {
  server.resetHandlers(
    rest.get("http://localhost:3030/scoops", (req, res, ctx) => res(ctx.status(500))),
    rest.get("http://localhost:3030/toppings", (req, res, ctx) => res(ctx.status(500)))
  );
  render(<OrderEntry />);
  
  await waitFor(async () => {
    const alerts = await screen.findAllByRole("alert");
    expect(alerts).toHaveLength(2);
  });
});

render 옵션

context API 등 wrapper 설정

  render(<Options optionType="scoops" />, { wrapper: OrderDetailsProvider });

전역 설정의 경우

import { RenderOptions, render } from "@testing-library/react";
import { OrderDetailsProvider } from "../contexts/OrderDetails";
import { ReactElement } from "react";

const renderWithContext = (ui: ReactElement, options?: Omit<RenderOptions, "wrapper">) =>
  render(ui, { wrapper: OrderDetailsProvider, ...options });

export * from "@testing-library/react";
export { renderWithContext as render };

사용하는 곳 import 경로 변경

// import { render, screen, waitFor } from "@testing-library/react"; 을 변경

import { render, screen, waitFor } from "../../../test-utils/test-utils";

공식 문서에서 제공하는 Router render 훅

export const renderWithRouter = (ui: JSX.Element, { route = "/" } = {}) => {
  window.history.pushState({}, "Test page", route);

  return {
    user: userEvent.setup(),
    ...render(ui, { wrapper: BrowserRouter }),
  };
};

 

경로(route)는 주로 라우팅 시스템을 테스트하거나 특정 경로에 따라 다른 동작을 수행하는 컴포넌트를 테스트할 때 사용된다. 

만약 컴포넌트 내부 요소만 테스트하고자 하는 경우 경로가 "/"이어도 상관없다.

byText 옵션

getByText(
  // If you're using `screen`, then skip the container argument:
  container: HTMLElement,
  text: TextMatch,
  options?: {
    selector?: string = '*',
    exact?: boolean = true,	// 
    ignore?: string|boolean = 'script, style',
    normalizer?: NormalizerFn,
  }): HTMLElement

selector

특정 요소를 지정

<label id="username">Username</label>
<input aria-labelledby="username" />
<span aria-labelledby="username">Please enter your username</span>
const inputNode = screen.getByLabelText('Username', {selector: 'input'})

exact

완전히 일치하는지 부분적으로 일치하는지

const scoopsSubtotal = screen.getByText("Scoops total: $", { exact: false });

jest.fn()

모킹과 스파이를 사용하면 다른 코드에 대한 의존성에 상관없이 테스트 가능한 단위로 분리하여 테스트의 정확성과 속도를 향상시킬 수 있다. 또한, 자동화된 테스트 환경에서 반복적으로 실행될 수 있기 때문에 실제 함수보다 가짜 함수를 사용하는 편이 훨씬 부담이 덜 하다.

jest.fn()이든 jest.mock()이든 render 함수 이전에 먼저 호출되어야한다.

props로 함수 프로퍼티가 필요한 경우

render(<LoginBox setModalType={jest.fn()} />);

만약 props 함수의 실행을 테스트 한고자 한다면 다음과 같은 주의 사항이 있다.

1. jest.fn()을 변수로 설정

  render(<LoginBox setModalType={jest.fn()} />);
  await userEvent.click(screen.getByLabelText("close"));
  expect(jest.fn()).toBeCalledWith("close");

만약 위처럼 변수나 상수로 사용하지 않고 jest.fn()을 각각 사용할 경우 실제 mock 함수를 호출하였는지 확인하지 않고 새로운 mock function을 생성하기 때문에 테스트에 실패하게 된다.

아래처럼 수정해 사용해야 함.

test("닫기를 누르면 로그인 모달이 닫힌다.", async () => {
  const mockSetModalType = jest.fn();
  render(<LoginBox setModalType={mockSetModalType} />);
  await userEvent.click(screen.getByLabelText("close"));
  expect(mockSetModalType).toBeCalledWith("close");
});

2. 함수 명시

CloseIcon 컴포넌트를 클릭했을 때 setModalType라는 함수가 실행된다면 CloseIcon 컴포넌트에서 아래처럼 onClick을 명시해주지 않고 ...props로 퉁칠 경우  userEvent.click해도  mockSetModalType가 호출되지 않는다.

export default function CloseIcon({ color, className, ...props }: CloseIconProps) {
  return (
    <div
      aria-label="close"
      className={`absolute top-3 right-3 cursor-pointer ${COLOR_VARIANT[color]} ${TYPOGRAPH_VARIANT["medium"]} ${className}`}
      {...props}
    >
      <AiOutlineClose />
    </div>
  );
}

아래처럼 명시적으로 적어줘야 함.

export default function CloseIcon({ color, className, onClick }: CloseIconProps) {
  return (
    <div
      aria-label="close"
      className={`absolute top-3 right-3 cursor-pointer ${COLOR_VARIANT[color]} ${TYPOGRAPH_VARIANT["medium"]} ${className}`}
      onClick={onClick}
    >
      <AiOutlineClose />
    </div>
  );
}

3. useEvent에 await 사용

await userEvent.click(screen.getByLabelText("close"));

웬만해선 이 형태를 기본으로 하는 게 좋다.

아래처럼 사용하면 실패하는 경우들이 종종 있어서 원인을 찾느라 엄한 곳에서 시간을 허비할 수 있음

userEvent.click(screen.getByLabelText("close"));

userEvent.click(await screen.findByLabelText("close"));

함수를 테스트 하는 경우

mockReturnValue(리턴 값) => 가짜 함수가 어떤 값을 리턴해야할지 설정

mockFn.mockReturnValue("I am a mock!");
console.log(mockFn()); // I am a mock!

mockResolvedValue(Promise가 resolve하는 값) => 가짜 비동기 함수 설정

mockFn.mockResolvedValue("I will be a mock!");
mockFn().then((result) => {
  console.log(result); // I will be a mock!
});

mockImplementation(구현 코드) => 가짜 함수 설정

mockFn.mockImplementation((name) => `I am ${name}!`);
console.log(mockFn("Dale")); // I am Dale!

 

설정한 mock 함수는 전부 기억됨

mockFn("a");
mockFn(["b", "c"]);

expect(mockFn).toBeCalledTimes(2);
expect(mockFn).toBeCalledWith("a");
expect(mockFn).toBeCalledWith(["b", "c"]);

jest.spyOn()

해당 함수가 예상대로 호출되었는지, 호출 횟수, 호출된 인자 등을 확인할 수 있다. (결과는 확인 x)

const calculator = {
  add: (a, b) => a + b,
};

const spyFn = jest.spyOn(calculator, "add");

const result = calculator.add(2, 3);

expect(spyFn).toBeCalledTimes(1);
expect(spyFn).toBeCalledWith(2, 3);
expect(result).toBe(5);

jest.mock()

모듈 등 그룹을 한꺼번에 모킹 처리 해줄때 사용한다. (jest.fn() 여러 개일 경우 그 전체를 알아서 모킹화해줌)

기본 사용법

jest.mock('모듈 경로 or 모듈 이름');
const test = require('모듈 경로 or 모듈 이름');

test.mockReturnValueOnce(~~~);

심화 예시)

next-auth 라이브러리를 사용했을 때,

초기값을 로그인 되어있는 상태로 설정하고, 로그인 되어있지 않을 때를 테스트하는 코드를 작성해보자.

// jest.setup

import "@testing-library/jest-dom/extend-expect";
import { server } from "./src/mock/server.js";

export const useSessionMock = jest.fn();

jest.mock("next-auth/react", () => {
  const originalModule = jest.requireActual("next-auth/react");

  const mockSession = {
    user: { username: "admin" },
    expires: new Date(Date.now() + 2 * 86400).toISOString(),
  };

  useSessionMock.mockReturnValue({ data: mockSession, status: "authenticated" });

  return {
    ...originalModule,
    useSession: useSessionMock,
    SessionProvider: ({ children }) => children,
  };
});

beforeAll(() => server.listen());

afterEach(() => {
  jest.resetAllMocks();
  server.resetHandlers();
});

afterAll(() => server.close());
// Login.test.tsx

import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import Home from "../app/page";
import RootLayout from "@/app/layout";
import { useSessionMock } from "../../jest.setup";

describe("로그인/회원가입 테스트", () => {
  test("로그인 버튼을 클릭하면 로그인 모달이 열린다", async () => {
    useSessionMock.mockReturnValueOnce({ data: null, status: "unauthenticated" });

    render(
      <RootLayout>
        <Home />
      </RootLayout>
    );

    userEvent.click(screen.getByRole("button", { name: "로그인" }));

    expect(await screen.findByLabelText("아이디")).toBeInTheDocument();
  });
});

useSessionMock를 상수로 선언했는데 할당 가능한 이유는 useSessionMock가 참조하고 있는 jest.fn()이 변경가능한 객체(mock 함수)를 반환하기 때문이다.

const mockFunction = jest.fn();
.
console.log(mockFunction());  // undefined

mockFunction.mockReturnValue('hello');
console.log(mockFunction());  // hello

mockFunction.mockReturnValue('world');
console.log(mockFunction());  // world

 

※ 참고 포스팅

https://www.daleseo.com/jest-fn-spy-on/#jestspyon-%EC%82%AC%EC%9A%A9%EB%B2%95

https://inpa.tistory.com/entry/JEST-%F0%9F%93%9A-%EB%AA%A8%ED%82%B9-mocking-jestfn-jestspyOn#mocking_%EB%A9%94%EC%86%8C%EB%93%9C_-_jest.mock

 

테스트 실패 메세지가 너무 길어서 html이 잘릴 때

const element = document.querySelector(`[data-testid="${type}_input"]`);
    console.log(element?.outerHTML);

이런 식으로 사용하면 원하는 요소 확인해볼 수 있음

 

'React > 테스팅 라이브러리' 카테고리의 다른 글

msw  (0) 2023.06.30
테스트 종료와 비동기 업데이트 충돌 오류  (0) 2023.06.27
user-event  (0) 2023.06.23
테스팅 쿼리  (0) 2023.06.22
테스팅 라이브러리 기본 개념  (0) 2023.06.21
Comments