테스트 코드 / jest / cypress

ssummer·2023년 9월 15일
post-thumbnail

테스트 방법

  • 단위테스트
    버튼 클릭과 같은 기능 하나하나를 테스트. 보통 jest를 사용한다.
  • 통합테스트
    여러 기능을 한꺼번에 테스트. 보통 jest를 사용한다.
  • E2E(End To End) 테스트
    로그인을 하고 결제 및 환불 등의 시나리오가 있는 테스트. 가상의 브라우저를 띄워 진행한다.

jest

🔗 https://jestjs.io/

jest 기본 세팅

  1. 설치
yarn add --dev jest jest-environment-jsdom @testing-library/react @testing-library/jest-dom
  1. jest.config.js 파일을 생성하고 아래 내용을 복사해 붙여넣는다.
// jest.config.js
const nextJest = require("next/jest");

const createJestConfig = nextJest({
  // Provide the path to your Next.js app to load next.config.js and .env files in your test environment
  dir: "./",
});

// Add any custom config to be passed to Jest
/** @type {import('jest').Config} */
const customJestConfig = {
  // Add more setup options before each test is run
  // setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
  // if using TypeScript with a baseUrl set to the root directory then you need the below for alias' to work
  moduleDirectories: ["node_modules", "<rootDir>/"],
  testEnvironment: "jest-environment-jsdom",
};

// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
module.exports = createJestConfig(customJestConfig);
  1. .eslintrc.js 파일에서 아래 내용으로 수정한다.
...
plugins: ["react","jest/globals"],
...
  1. package.json 파일에 아래의 스크립트를 추가한다.
"scripts": {
    "test": "jest",
		"test:watch": "jest --watch"
  }

테스트 코드 작성

// index.tsx
export const add = (a: number, b: number): number => {
  return a + b;
};

테스트 코드를 작성해보기 위해 위와 같은 함수를 작성해본다. 테스트 코드는 테스트할 파일과 같은 확장자로 생성하고 이름에 test가 포함되어야한다.

React에서는 테스트할 파일이 있는 위치에 __test__ 폴더를 생성해 테스트 파일을 위치시켜야하고 NextJS에서는 pages 폴더의 바깥에 __test__ 폴더를 생성하고 그 안에 테스트 코드 파일을 위치시킨다.

// index.test.tsx
import { add } from "../../pages/example/jest"; // 테스트할 파일에서 호출

it("더하기가 잘 되는지 테스트 해보기", () => {
  const result = add(3, 5);
  expect(result).toBe(8);
});

터미널에 yarn test 명령어를 입력하면 jesttest가 들어있는 모든 파일을 찾아 테스트를 진행한다.

테스트에 통과한 것을 확인할 수 있다.

describe("테스트그룹 만들기", () => {
  it("더하기 테스트", () => {});
  it("빼기 테스트", () => {});
});

테스트 코드를 작성할 때 여러 테스트를 묶어 하나의 그룹으로 진행하려면 위처럼 작성한다.


UI(presenter) 테스트 코드 작성

// index.tsx
export default function JestUnitTestPage(): JSX.Element {
  return (
    <>
      <div>제목</div>
	  내용 : <input type="text" />
      <button>저장</button>
    </>
  );
}
// index.test.tsx
import JestUnitTestPage from "../../pages/example/jest-unit-test";
import { render, screen } from "@testing-library/react";
// 가상의 돔을 그리려면 위의 render를 사용해야하고
import "@testing-library/jest-dom";
// 위의 render를 사용하려면 위의 jest-dom을 사용해야함

it("내가 원하는대로 그려지는지 테스트하기", () => {
  render(<JestUnitTestPage />);

  const myText = screen.getByText("제목");
  expect(myText).toBeInTheDocument();

  const myText2 = screen.getByText("내용 :");
  expect(myText2).toBeInTheDocument();

  const myText3 = screen.getByText("저장");
  expect(myText3).toBeInTheDocument();
});

스냅샷 테스트

UI 테스트를 할 때 확인해야할 요소가 많다면 테스트 코드를 작성하는데 번거로워진다. 이때 스냅샷 테스트를 한다. 말 그대로 사진을 찍어두는 것인데 수정사항이 있을 때 찍어둔 사진과 비교해 다른 부분을 알려준다.

위와 동일한 파일에 대해 스냅샷 테스트 코드를 작성해본다.

// index.test.tsx
import JestUnitTestPage from "../../pages/example/jest-unit-test-snapshot";
import { render, screen } from "@testing-library/react";
import "@testing-library/jest-dom";

it("스냅샷 테스트", () => {
  const result = render(<JestUnitTestPage />);
  expect(result.container).toMatchSnapshot();
});

기존에 찍어둔 스냅샷이 존재하지 않는다면 새로 찍어준다. 코드 수정 후 스냅샷을 새로 찍고 싶다면 아래의 명령어로 테스트를 진행한다.

yarn test -u (update)

생성된 스냅샷은 테스트 코드가 있는 디렉토리에 __snapshots__ 폴더 안에 저장된다.


container 테스트 코드 작성

기능을 테스트하는 코드이다.

// index.tsx
import { useState } from "react";

export default function JestUnitTestPage(): JSX.Element {
  const [count, setCount] = useState(0);
  const onClickCountUp = (): void => {
    setCount((prev) => prev + 1);
  };
  return (
    <>
      <div role="count">{count}</div>
      <button role="count-button" onClick={onClickCountUp}>
        카운트 올리기
      </button>
    </>
  );
}

기능 테스트를 할 땐 어떤 태그가 어떤 일을 하는지 알려줘야 한다. <button>role="count-button"을 추가한다.

// index.test.tsx
import JestUnitTestPage from "../../pages/example/jest-unit-test-event";
import { fireEvent, render, screen } from "@testing-library/react";
import "@testing-library/jest-dom";

it("버튼을 눌렀을 때, 제대로 작동하는지 테스트!", () => {
  render(<JestUnitTestPage />);

  fireEvent.click(screen.getByRole("count-button"));
  expect(screen.getByRole("count")).toHaveTextContent("1");
});

api 테스트 코드 작성

api 요청이 제대로 수행되는지 테스트를 할 땐 실제 백엔드에 요청을 하는 것이 아니다. 이땐 mocking을 이용해 백엔드 없이 프론트엔드에서만 요청을 만들어 테스트를 한다.

mockingapi 요청을 가짜로 만드는 것이기 때문에 각 라이브러리마다 제공해주는 기능이 존재한다.

  • axios - axios-mock-adapter
  • apollo-client - apollo-mock-provider
yarn add -D msw cross-fetch next-router-mock

(msw - mock service worker)

// index.tsx
import { gql, useMutation } from "@apollo/client";
import { ChangeEvent, useState } from "react";
import { wrapAsync } from "../../../src/commons/libraries/asyncFunc";
import { useRouter } from "next/router";

const gqlSetting = gql`
  mutation createBoard($createBoardInput: CreateBoardInput!) {
    createBoard(createBoardInput: $createBoardInput) {
      _id
      writer
      title
      contents
      password
    }
  }
`;

export default function PaginationPage(): JSX.Element {
  const router = useRouter();

  const [writer, setWriter] = useState("");
  const [title, setTitle] = useState("");
  const [contents, setContents] = useState("");
  const [myfunction] = useMutation(gqlSetting);

  const onClickSubmit = async (): Promise<void> => {
    const result = await myfunction({
      variables: {
        createBoardInput: {
          writer,
          title,
          contents,
          password: "1234",
        },
      },
    });
    const boardId = result?.data?.createBoardInput._id;
    void router.push(`/boards/${boardId}`);
  };

  const onChangeWriter = (event: ChangeEvent<HTMLInputElement>): void => {
    setWriter(event.target.value);
  };
  const onChangeTitle = (event: ChangeEvent<HTMLInputElement>): void => {
    setTitle(event.target.value);
  };
  const onChangeContents = (event: ChangeEvent<HTMLInputElement>): void => {
    setContents(event.target.value);
  };
  return (
    <div>
      작성자 :
      <input role="input-writer" type="text" onChange={onChangeWriter}></input>
      제목 :
      <input role="input-title" type="text" onChange={onChangeTitle}></input>
      내용 :
      <input
        role="input-contents"
        type="text"
        onChange={onChangeContents}
      ></input>
      <button role="submit-button" onClick={wrapAsync(onClickSubmit)}>
        GRAPHQL-API 요청하기
      </button>
    </div>
  );
}
// index.test.tsx
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import PaginationPage from "../../pages/example/jest-unit-test-mocking";
import {
  ApolloClient,
  ApolloProvider,
  HttpLink,
  InMemoryCache,
} from "@apollo/client";
import fetch from "cross-fetch";
import mockRouter from "next-router-mock";

// 가짜 useRouter
jest.mock("next/router", () => require("next-router-mock"));

it("게시글이 잘 등록되는지 테스트", async () => {
  const client = new ApolloClient({
    link: new HttpLink({
      uri: "http://mock.com/graphql",
      fetch,
    }),
    cache: new InMemoryCache(),
  });
  render(
    <ApolloProvider client={client}>
      <PaginationPage />
    </ApolloProvider>,
  );
 fireEvent.change(screen.getByRole("input-writer"), {
    target: { value: "훈이" },
  });

  fireEvent.change(screen.getByRole("input-title"), {
    target: { value: "어쩔" },
  });

  fireEvent.change(screen.getByRole("input-contents"), {
    target: { value: "저쩔" },
  });

  fireEvent.click(screen.getByRole("submit-button"));

  await waitFor(() => {
    expect(mockRouter.asPath).toEqual("/boards/qqq");
  });
});

yarn test를 실행하면 기존에 있는 모든 소스코드들은 무시하고 test가 들어있는 파일들만 실행된다. 당연히 그 파일들에는 apolloSetting도 없으므로 테스트 파일 내에서 설정해줘야한다. 테스트 코드 내 render<App />로 봐야한다.


cypress

NextJS에서 cypress로 e2e테스트하기
https://nextjs.org/docs/pages/building-your-application/optimizing/testing

yarn add --dev cypress
...
"scripts": {
  "test:e2e": "cypress open"
},
...

터미널에 yarn test:e2e 명령어를 실행시키면 cypress가 실행된다.

E2E Testing 버튼을 누르면 기본 세팅을 알아서 진행해준다.

세팅이 완료되면 node_modules와 같은 경로에 cypress 폴더가 생성된다.

크롬에서 실행하기를 누르면 가상의 크롬 브라우저가 띄워진다.

Create new spec 버튼을 눌러 직접 스펙을 만든다.

스펙 명을 입력해주면 샘플 코드가 작성된 스펙이 등록되고 테스트도 자동으로 실행해준다.

여기서 페이지가 잘 이동이 되는지를 테스트할 것이다.

// e2e-test/index.tsx
import { useRouter } from "next/router";

export default function CypressE2eTestPage(): JSX.Element {
  const router = useRouter();

  const onClickMove = (): void => {
    void router.push("/e2e-test-moved");
  };

  return <button onClick={onClickMove}>다람쥐랑 놀러가기</button>;
}
// e2e-test-moved/index.tsx
export default function CypressE2eTestMovedPage(): JSX.Element {
  return <div>다람아 놀자</div>;
}
// cypress/e2e/01-pagemove.cy.ts
it("페이지 이동 시나리오", () => {
  cy.visit("http://localhost:3000/cypress-e2e-test");
  cy.get("button").click();
  cy.get("div").contains("다람아 놀자");
});

e2e 테스트는 실서버에서 진행되는 테스트이기 때문에 yarn dev로 프론트엔드 서버를 실행시킨 다음에 진행해야 한다.

yarn test:e2e

테스트를 실행시키고 cypress에서 생성한 스펙을 클릭하면

페이지 이동 테스트가 정상적으로 성공했다는 화면을 볼 수 있다.


TDD(Test Driven Development)

보통의 개발 과정은 요구 사항을 정의, 디자인 산출, 실제 코드 작성 후 테스트 진행 정도로 이어진다. TDD, 즉 테스트 주도 개발은 테스트 코드를 먼저 작성하고 그 후에 테스트를 통과하기 위한 최소한의 실제 코드를 작성한다. 그 후에 코드를 리팩토링한다.

TDD를 하는 이유는

  • 테스트를 나중에 만들려면 바쁘고 귀찮아서 안하게 됨(기술부채)
  • 테스트를 통과하게끔 일부러 쉽게 만들 수도 있음
  • 테스트 문화를 만들기 위함

등이 있다. 테스트 코드를 짜야하는건 맞지만 TDD가 필수는 아니다.

테스트코드 작성 시기
테스트 코드 작성은 스타트업 기준 첫 배포 후 하는 것이 일반적임. 버전 1은 런칭에 더 초점이 맞춰져있음.

0개의 댓글