2023. 5. 1

Junghan Lee·2023년 5월 1일
0

TIL Diary

목록 보기
44/52

index

테스트코드(jest)
단위테스트 snapshot test
TDD

intro

테스트코드 : 내가 만든 코드를 한번 더 검사하는 코드
TDD : 기능도 없이 테스트?

테스트 코드

테스트라고하면 마우스로 클릭을 통해 api를 요청하는 작업같은 것들을 대신해주는 것

버그잡는 과정을 테스트코드는 조금 더 수월하도록 도와준다.
서비스의 사이즈가 커질수록 테스트 코드의 유용함이 커지며, 버그 수정의 과정이 편리해진다.

테스트 코드는 언제 작성?
스타트업 기준) 버전 1 의 배포가 끝난 후 만드는 것이 일반적으로 가장 적절, 버전 1의 개발 시점에서는 테스트코드보다 런칭에 조금 더 ㄹ초점이 맞춰지기 때문에 배포 후에 만드는 것이 비즈니스적으로 가장 적절, 그러나 회사마다 다르다.

테스트 방법
1) 단위 테스트
단위테스트는 버튼클릭과 같은 기능 하나하나를 테스트(프레임워크: jest)
2) 통합 테스트 - 사용 프레임워크 : jest
여러 기능 한꺼번에 테스트
3) E2E(End To End) 테스트
로그인 후 결제, 환불할 떄 같은 시나리오가 있는 테스트를 할 때 사용, 가상의 브라우저를 띄워 테스트 진행

jest 설치
yarn add --dev jest jest-environment-jsdom @testing-library/react @testing-library/jest-dom

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);

jest, esLint 함께 사용
jest를 eslint를 함께 사용하기위해서 eslintrc.js 파일로 들어가 아래 내용으로 바꿔야 한다.

...
plugins: ["react","jest/globals"],
...

package.json에 jest 실행 명령어 추가

"scripts": {
    "test": "jest",
		"test:watch": "jest --watch"
  }

test:watch는 소스 코드를 고칠 때마다 jest가 실행되길 원하면 추가하면 된다. 추가하지 않는다면 원할 때마다 yarn test를 입력해 jest 실행해주어야 함

테스트코드 작성
값을 더해주는 기능을 하는 함수, 동작 테스트

// index.ts 파일 -> 실제 기능
export const add = (a: number, b: number) => {
  return a + b;
};

React에서는 폴더 안에 test폴더를 생성해 index.test.ts 파일을 만들어주어야 하지만 Next에서는 test폴더를 pages 바깥에 위치시킴(Next에서 pages안의 폴더는 페이지가 되기 때문)

index.ts 파일의 확장자가 tsx면 index.test.ts파일의 확장자 또한 tsx가 되어야 한다.

// index.test.ts 파일 -> 테스트 코드

import { add } from "./sum";

// 앞부분 string은 테스트 제목이며, 실패시에 어디서 실패했는지 보여주는 부분이 됩니다.
it("2와 3이 주어졌을 때, 5가 나와야 한다.", () => {
	// 테스트 할 내용 -> 문제도 정답도 본인이 만들어야 합니다.
	const result = add(2,3)
  expect(result.toBe(5);
});

터미널에서 yarn test 입력, jest는 파일명에 test가 들어간 모든 파일을 찾아 테스트 진행

위와 같은 테스트를 한번에 여러 개 하고싶다 -> 테스트 그룹 생성 : describe

describe("나만의 테스트 그룹만들기",()=>{
	it("내가 하고싶은 테스트1",()=>{})
	it("내가 하고싶은 테스트2",()=>{})
	it("내가 하고싶은 테스트3",()=>{})
})

일반적인 단위 테스트

위의 테스트 코드는 자바스크립트만 다루는 파일의 테스트 코드였으나 react 의 컴포넌트, jsx의 단위테스트를 진행할 때는?

UI(presenter) 테스트 코드 작성

// 34-02-jest-unit-test/index.tsx -> 실제 기능
export default function JestUnitTestPage(){
	return(
		<>
			<div> 철수는 13살 입니다.</div>
			철수의 취미 입력하기 : <input type="text" />
			<button> 철수랑 놀러가기 <button/>
		</>
	)
}

만들어놓은 test폴더 안에 테스트코드 작성! 폴더 안에 폴더를 만든 후 index.test.tsx 파일 생성(실제 기능파일의 확장자와 동일)

// __test__/ 34-02-jest-unit-test/index.test.tsx

import JestUnitTestPage from '폴더경로'
import {render,screen} from '@testing-library/react'
import "@testing-library/jest-dom"

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

컴포넌트가 제대로 그려지는지 확인하려면 가짜로 그려질 수 있도록 도와주는 도구 render를 사용해야 한다. 그럼 컴포넌트가 render되고 결과는 screen에 들어오게 됨. 렌더링 결과를 화면에 그려야 하는데 이는 가짜 돔인 jest-dom에 그려줄 것이다.

// __test__/ 34-02-jest-unit-test/index.test.tsx

import JestUnitTestPage from '폴더경로'
import {render,screen} from '@testing-library/react'
import "@testing-library/jest-dom"

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

	const myText1 = screen.getByText("철수는 13살 입니다.")
	expect(myText1).toBeInTheDocument()

	const myText2 = screen.getByText("철수의 취미 입력하기:")
	expect(myText2).toBeInTheDocument()

	const myText3 = screen.getByText("철수랑 놀러가기")
	expect(myText3).toBeInTheDocument()
})

만들어준 후 yarn test -> 실행 결과

에러 발생시 @testing-library/react의 버전 확인, ^12.1.2 버전이 아니라면 재설치해 버전 맞추어야 함.
(노드 모듈 삭제 => yarn.lock파일 삭제 => package.json 파일에서 원하는 버전 설정, 저장 => yarn install)

이러면 일일히 하나식 모두 적어줘야 하기 때문에 컴포넌트에서 확인할 것이 많을 수록 귀찮아짐 -> snapShot-test 등장

snapShot test
스냅샷 테스트는 매번 볼 수 없기 때문에 사진을 찍어둔 후 수정사항이 있을 때 기존의 사진과 비교해서 다른 부분을 알려준다. 일부러 수정한 부분은 다시 사진을 찍어주면 된다.

// 34-03-unit-test-snapshot/index.tsx -> 실제 기능
export default function JestUnitTestPage(){
	return(
		<>
			<div> 철수는 13살 입니다.</div>
			철수의 취미 입력하기 : <input type="text" />
			<button> 철수랑 놀러가기 <button/>
		</>
	)
}
// __test__/ 34-03-jest-unit-snapshot/index.test.tsx

import JestUnitTestPage from '폴더경로'
import {render,screen} from '@testing-library/react'
import "@testing-library/jest-dom"

it("내가 원하는대로 그려지는지 테스트",()=>{
	const result = render(<JestUnitTestPage />)
	expect(result.container).toMatchSnapshot()
})

이렇게 작성해주시면, 앞으로 snapshot과 비교하게 된다. 만일 이전에 찍어둔 snapshot이 없다면 알아서 찍어준다.

container 테스트코드

기능 테스트 -> 버튼 눌렀을 때 제대로 작동하는지 테스트 ; 실제 기능을 담은 폴더를 pages 내부에 생성, 실제 기능을 담아둘 파일을 생성할 때는 확장자를 tsx로 바꿔주어야 함.

// 실제 기능
import {useState} from 'react'

export default function CounterStatePage(){
    const [count, setCount] = useState(0)

    const qqq = Math.random()

    console.log(qqq)

    function onClickCountUp(){
       setCount(prev => prev+1)
    }

    return (
        <>
            <div>{count}</div>
            <button onClick={onClickCountUp} role="count-button">카운트 올리기!!!</button>
        </>
    )

}

실제 기능에서 해당 기능을 테스트할 때 : 어떤 태그가 어떤 일을 하는지 알아야 하므로
button태그에 role="count-button"을 추가해주면 됨

이 기능을 테스트하기 위해 test폴더 안에 새로운 폴더 생성, 역시 안에 index.test.tsx파일을 만들어주면 됨.

// 테스트 코드
import CounterStatePage from '폴더경로'
import {render,fireEvent} from '@testing-library/react'
import "@testing-library/jest-dom"

it("버튼을 눌렀을 때 제대로 작동하는지 테스트",()=>{
	render(<CounterStatePage />)
	fireEvent.click(screen.getByRole("count-button"))
	expect(screen.getByRole("count")).toHaveContent("1")
})

container 내부의 api요청 테스트 코드 작성하기

테스트는 실제 백엔드로 요청하지 않는다. 서버에 부하가 갈 수 있기 때문이다. mocking을 이용해 요청, 응답을 가짜로 생성하여 백 없이 프론트에서만 테스트한다.

yarn add -D msw cross-fetch next-router-mock

//34-05-jest-unit-test-mocking/index.tsx
import { gql, useMutation } from "@apollo/client";
import { useRouter } from "next/router";
import { ChangeEvent, useState } from "react";

// prettier-ignore
export const CREATE_BOARD = gql`
  mutation createBoard($createBoardInput: CreateBoardInput!) { # 변수의 타입 적는 곳
    createBoard(createBoardInput: $createBoardInput) { # 실제 우리가 전달할 변수 적는 곳
      _id
      writer
      title
      contents
    }
  }
`;

export default function GraphqlMutationPage() {
  const router = useRouter();

  const [writer, setWriter] = useState("");
  const [title, setTitle] = useState("");
  const [contents, setContents] = useState("");
  const [나의함수] = useMutation(CREATE_BOARD);

  const onClickSubmit = async () => {
    console.log("await 윗부분");
    // const writer = "qqq" // 이 함수에 있으면 현재 스코프
    const result = await 나의함수({
      variables: {
        createBoardInput: {
          // variables 이게 $ 역할을 해줌
          writer: writer, // 이 함수에 없으면 스코프 체인을 통해서 위 함수에서 찾음
          title: title,
          contents: contents,
          password: "1234",
        },
      },
    });
    console.log("await 아랫부분");
    console.log(result);
    // alert(result.data.createBoard.message);
    router.push(`/boards/${result.data.createBoard._id}`);
  };

  const onChangeWriter = (event: ChangeEvent<HTMLInputElement>) => {
    setWriter(event.target.value);
  };

  const onChangeTitle = (event: ChangeEvent<HTMLInputElement>) => {
    setTitle(event.target.value);
  };

  const onChangeContents = (event: ChangeEvent<HTMLInputElement>) => {
    setContents(event.target.value);
  };

  return (
    <>
      작성자:
      <input role="input-writer" type="text" onChange={onChangeWriter} />
      <br />
      제목: <input role="input-title" type="text" onChange={onChangeTitle} />
      <br />
      내용:
      <input role="input-contents" type="text" onChange={onChangeContents} />
      <br />
      <button role="submit-button" onClick={onClickSubmit}>
        GRAPHQL-API(동기) 요청하기
      </button>
    </>
  );
}
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import "@testing-library/jest-dom";
import JestUnitTestMockingPage from "pages/34-05-jest-unit-test-mocking";

import {
  ApolloClient,
  ApolloProvider,
  HttpLink,
  InMemoryCache,
} from "@apollo/client";

import mockRouter from "next-router-mock";
import fetch from "cross-fetch";

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

it("버튼 테스트 - msw", async () => {
  const client = new ApolloClient({
    link: new HttpLink({
      uri: "http://mock.com/graphql",
      fetch,
    }),
    cache: new InMemoryCache(),
  });

  render(
    <ApolloProvider client={client}>
      <JestUnitTestMockingPage />
    </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.change(screen.getByRole("input-password"), {
    target: { value: "1234" },
  });

  fireEvent.click(screen.getByRole("submit"));
  await waitFor(() => {
    expect(mockRouter.asPath).toEqual("/board/qqq");
  });
});

가짜 api를 사용하기 위해서 api를 모아둘 수 있는 파일을 새로 만들어 주고 해당 파일은 src/commons/mocks 폴더 안에 apis.js 파일로 새로 생성한다.

import { graphql } from "msw";

const gql = graphql.link("http://mock.com/graphql");
export const apis = [
  gql.mutation("createBoard", (req, res, ctx) => {
    const { wrtier, title, contents } = req.variables.createBoardInput;

    return res(
      ctx.data({
        createBoard: {
          _id: "qqq",
          wrtier,
          title,
          contents,
          __typename: "Board",
        },
      })
    );
  }),
];

위의 api 파일을 사용하기 위해서 서버를 셋팅할 수 있는 js 파일을 동일한 디렉토리 안에 새로 생성한다.

import { setupServer } from "msw/node";
import { apis } from "./apis";

// 목킹 데이터를 가짜 서버로 돌릴 수 있도록 설정
export const server = setupServer(...apis);

jest가 실행될 때마다 서버를 그때그때마다 수동으로 작동시키기는 매우 비효율적이므로 jest가 자동으로 서버를 실행시킬 수 있도록 설정이 필요 하다.

상위 디렉토리, jest.config.js와 동일한 경로의 디렉토리 안에 jest.setup.js 파일을 생성한 후
서버를 실행시키는 코드를 새로 추가

import { server } from "./src/commons/mocks";

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

jest가 서버를 자동으로 실행시킬 수 있도록, jest.config.js에서 jest.setup.js가 서버를 실행시켜주는 파일임을 명시해줘야 한다.

// jest.config.js

const customJestConfig = {
	...
	...
	...
  // jest 실행시마다 실행되는 셋팅 파일
  setupFilesAfterEnv: ["./jest.setup.js"],
};

마지막으로, eslint가 jest를 감지할 수 있도록
eslintrc.js의 env탭에 jest를 추가

// eslintrc.js

module.exports = {
  env: {
    browser: true,
    es2021: true,
    jest: true,
  },
	...
	...
}

여기까지 완료가 끝났다면 내가 원하는 api를 jest로 검증할 수 있.

yarn jest 명령어를 입력해서 api로 받아오는 결과물이 제대로 된 결과물인지를 검증한다.

TDD
Test-Driven-Development , 테스트 주도 개발을 의미한다. 보통의 개발 과정은 다음과 같다. 요구 사항을 정의하고, 디자인이 나오고, 이를 토대로 실제 코드를 작성 하고 테스트를 진행한다.

하지만 TDD는 테스트 코드를 먼저 작성하고 그 후에 테스트를 통과하기 위한 최소한의 실제 코드를 작성한다. 그 다음에 코드를 리팩토링!

코딩문화!

profile
Strive for greatness

0개의 댓글