[TIL] 테스트 코드

신재욱·2023년 5월 1일
1
post-thumbnail

📒 오늘 공부한 내용

🔍수업목차

[32-1] 테스트 코드
[32-2] 일반적인 단위테스트

✅ 테스트 코드


마우스로 클릭을 통해 api를 요청하는 작업같은 것들을 대신해주는 것 이다.

📂 테스트코드의 필요성

  • 사이트를 런칭해 1차개발이 완료되었다. 이후에 2주정도 버그가 있는지 확인하고 수정하고 잘 돌아가나 테스트하는 시간을 2주정도 가지게 된다.
  • 2주간 버그를 열심히 잡은 결과 깔끔한 어플리케이션이 되었다고 했을 때 서비스를 배포 했다.
  • 이후에 2차개발에 돌입해 기능을 하나 더 만들고 배포 3일후 멀쩡하던 다른 기능에서 에러가 발생하기 시작했다.
  • 왜 버그를 다 잡고 배포를 했는데 왜 에러를 보게 되었을까?
  • 2차 배포 다시 말해 업데이트 배포한 코드들이 이전 배포한 기능에 영향을 주고 있는 것 이다.
  • 예를 들어 업데이트 배포때 배포한 결제기능이 1차 배포때 배포한 상품관련 기능에 연관된 코드를 가지고 있어 에러를 보게 되는 것 이고 하나의 기능에만 영향을 미친다고 확신할 수도 없다.
  • 이런 힘든 버그잡는 과정을 테스트코드는 조금 더 수월하도록 도와주고있다.
  • 서비스의 사이즈가 커질수록 테스트 코드의 유용함이 커지며, 버그 수정의 과정이 편리해진다.

💡 언제부터 테스트코드를 작성할까?

  • 스타트업 : 버전1의 배포가 끝난 후 만드는것이 일반적으로 가장 적절하다.
  • 버전1의 개발 시점에서는 테스트코드보다 런칭에 조금 더 초점이 맞춰지기 때문에 배포후에 만드는 것이 비지니스 적으로 가장 적절한 때다.
  • 하지만, 회사마다 다르기 때문에 무조건적인것은 아니다.

📂 다양한 테스트 방법

  • 테스트를 하는 방법은 다양하다.

1️⃣ 단위테스트

  • 단위테스트는 버튼클릭과 같은 기능 하나하나를 테스트한다.

  • 테스트를 위해 사용하는 프레임워크는 보통 jest를 사용한다.

2️⃣ 통합테스트 - 사용 프레임 워크 : jest

  • 여러 기능을 한꺼번에 테스트한다.

  • 테스트를 위해 사용하는 프레임워크는 보통 jest를 사용한다.

3️⃣ E2E(End To End) 테스트

  • 로그인 후 결제를 하고 환불할 때 같은 시나리오가 있는 테스트를 할 때 사용한다.

  • E2E 테스트를 진행할때는 가상의 브라우저를 띄워 테스트를 진행한다.

📂 테스트코드 만들기 _ jest

jest 설치

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

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

package.json에 jest를 실행시키기 위한 명령어를 추가

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

테스트 코드 작성

📌 index.ts 파일 -> 실제 기능

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

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

import { add } from "./sum";

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

테스트 그룹

  • 테스트 그룹을 만들때는 describe 를 이용해서 만들 수 있다.
    describe("나만의 테스트 그룹만들기",()=>{
    	it("내가 하고싶은 테스트1",()=>{})
    	it("내가 하고싶은 테스트2",()=>{})
    	it("내가 하고싶은 테스트3",()=>{})
    })

✅ 일반적인 단위테스트


📂 UI(presenter) 테스트코드 작성

📌 34-02-jest-unit-test/index.tsx -> 실제 기능

export default function JestUnitTestPage(){
	return(
		<>
			<div> 철수는 13살 입니다.</div>
			철수의 취미 입력하기 : <input type="text" />
			<button> 철수랑 놀러가기 <button/>
		</>
	)
}

📌 __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()
})

💡 버전 맞추기

  • rm -rf node_modules : 노드모듈 삭제 명령어
  • rm -rf yarn.lock : yarn.lock 파일삭제 → 이전에 설치했던 버전들이 기억되어있기때문에 삭제
  • package.json 파일에서 원하는 버전 설정후 저장
  • yarn install : 지웠던 노드모듈과 yarn.lock 파일을 재생성

📂 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()
})

📂 container 테스트코드

  • 기능 테스트코드는 버튼을 눌렀을 때 제대로 작동하는지를 테스트 해주면 된다.

📌 실제 기능

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

}

📌 테스트 코드

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요청 테스트 코드

  • api요청이 제대로 수행되는지 확인하기 위해서 테스트 코드를 작성하는데, 주의할 점이 있다.
  • 실제 백엔드로 요청을 하는것이 아니라는 점 이다.
  • 백엔드로 요청을 보내게 되면, 테스트시에 200-300개 정도의 요청을 보내게 될 수 있다. 그럼 서버에 부하가 가기때문에 좋지 못한 방법이다.

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

📌 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.config.js와 동일한 경로의 디렉토리 안에 jest.setup.js 파일을 생성한 후 서버를 실행시키는 코드를 새로 추가

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

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

📌 jest.config.js

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

📂 TDD

  • TDD는 테스트 코드를 먼저 작성하고 그 후에 테스트를 통과하기 위한 최소한의 실제 코드를 작성합니다. 그 다음에 코드를 리팩토링한다.
profile
1년차 프론트엔드 개발자

0개의 댓글