[코드캠프]33일차_TIL_테스트코드

윤성해·2023년 5월 1일
0

프론트엔드_TIL

목록 보기
27/27
post-thumbnail

수업 목차

  1. 테스트 코드
  2. 일반적인 단위테스트 실습
  3. TDD

오늘의 TIL

테스트 코드

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

테스트 코드의 필요성

어떤 사이트를 런칭하기 위해 개발자들이 몇달간 열심히 개발을 해서 1차개발이 완료되었다고 가정해보겠습니다.그럼 이후에 2주정도 버그가 있는지 확인하고 수정하고 잘 돌아가나 테스트하는 시간을 2주정도 가지게 됩니다.
2주간 버그를 열심히 잡은 결과 깔끔한 어플리케이션이 되었다고 했을 때 개발자들은 배포를 했습니다.

그리고 2차개발에 돌입해 위와 같은 과정으로 기능을 하나 더 만들고 배포 3일 후 갑자기 멀쩡하던 다른 기능에서 에러가 발생하기 시작했습니다.
개발자들은 분명 버그를 다 잡고 배포를 했는데 왜 에러를 보게 되었을까요?

2차 배포 다시 말해 업데이트 배포한 코드들이 이전 배포한 기능에 영향을 주고 있는 것 입니다.

예를 들면 업데이트 배포때 배포한 결제기능이 1차 배포때 배포한 상품관련 기능에 연관된 코드를 가지고 있어 에러를 보게 되는 것 입니다.

그럼 과연 하나의 기능에만 영향을 미친다고 확신할 수 있을까요?
만약 아니게 된다면 버전1부터 길고긴 버그수정의 기간을 가져야 합니다.

이런 힘든 버그잡는 과정을 테스트코드는 조금 더 수월하도록 도와주고있습니다.
서비스의 사이즈가 커질수록 테스트 코드의 유용함이 커지며, 버그 수정의 과정이 편리해집니다.

⚠️ 그럼 언제부터 테스트코드를 작성할까?
→ 스타트업을 기준으로, 버전1의 배포가 끝난 후 만드는것이 일반적으로 가장 적절하다고 한다.
버전1의 개발 시점에서는 테스트코드보다 런칭에 조금 더 초점이 맞춰지기 때문에 배포후에 만드는 것이 비지니스 적으로 가장 적절한 때입니다.
회사마다 다르기 때문에 무조건적인것은 아닙니다.

다양한 테스트 방법

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

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

테스트코드 만들어보기 with jest

그럼 위의 테스트 방법은 어떤 도구를 이용해서 진행해야 할까요?

테스트 코드를 만들 수 있도록 도와주는 프레임워크는 여러가지가 있지만, 우리는 그 중에 Jest를 사용하여 개별 단위테스코드를 만들어보겠습니다.

1. jest 설치

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

2. jest.config.js 파일 생성

  1. jest.config.js 이름으로 새파일 생성
  2. 아래 내용 붙여넣기
// 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);

3. jest, esLint 함께 사용하기

jest를 eslint를 함께 사용하기위해서는 추가해줘야 할 내용이 있습니다.
.eslintrc.js 파일로 들어가 아래 내용으로 바꿔주세요!

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

4. package.json에 jest를 실행시키기 위한 명령어를 추가해줍니다.

package.json 파일에 스크립트를 추가합니다.

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

test:watch는 소스코드를 고칠때마다 jest가 실행되길 원하신다면 추가해주시면 됩니다.
만일 추가하지 않으신다면, 원할때마다 yarn test 를 입력하셔서 jest를 실행해주셔야 합니다. 여기까지 완료하셨다면, jset를 사용하기 위한 기본적인 세팅은 완료되었습니다.

5. 테스트 코드 작성

값을 더해주는 기능을 하는 함수를 작성해보고, 제대로 동작하는지 테스트 해보겠습니다. 34-01-jest폴더 를 만들어주고, index.ts 파일을 생성했습ㄴ다.

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

React에서는 34-01-jest폴더__test__ 폴더 를 생성해 index.test.ts 파일을 만들어줘야 하지만,

Next에서는 __test__ 폴더34-01-jest폴더 에 넣지않고 pages 바깥에 위치시킵니다.
왜냐하면, Next에서 pages안의 폴더는 페이지가 되기 때문입니다.

따라서 우리는 pages 폴더 바깥쪽에 __test__ 폴더 를 생성하고, 해당폴더 안쪽에 34-01-jest폴더 를 넣어주도록 하겠습니다.
그리고 만일 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가 들어간 모든 파일을 찾아 테스트를 진행합니다.

6. 나만의 테스트그룹 만들기

만일 위와 같은 테스트를 한번에 여러개 만들고 싶으면, 테스트 그룹을 만들어주면 됩니다. 아래와 같이 describe 를 이용해서 만들 수 있습니다.

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

일반적인 단위테스트 실습

위 코드는 자바스크립트만 다루는 파일의 테스트코드입니다. 우리는 리액트 컴포넌트를 다루고 있기때문에 jsx를 사용합니다.

UI(presenter) 테스트코드 작성 해보기

pages폴더 안에 34-02-jest-unit-test 폴더 만들었음! 안에는 index.tsx 파일 만들었습니다.

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

이제 테스트 코드를 만들어봅시다.
우리가 위에서 만들어놨던 __test__ 폴더 안쪽에 테스트코드를 작성해주도록 합니다.
__test__ 폴더 안쪽에 34-02-jest-unit-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를 하면 실행 결과가 나옵니다!!

❗️ yarn test 로 실행했는데 에러가 난다면?
→ package.json에서 @testing-library/react 의 버전을 확인해주세요!
@testing-library/react: "^12.1.2" 버전이 아니라면 재설치를 통해 버전을 맞춰주세요!

⚠️ 버전 맞추는 방법
1. rm -rf node_modules : 노드모듈 삭제 명령어
2. rm -rf yarn.lock : yarn.lock 파일삭제 → 이전에 설치했던 버전들이 기억되어있기때문에 삭제
3. package.json 파일에서 원하는 버전 설정후 저장
4. yarn install : 지웠던 노드모듈과 yarn.lock 파일을 재생성합니다.

그런데 이렇게하면 일일이 모두 적어야해서 굉장히 귀찮아집니다. 귀찮음을 조금 덜기위해서 snapShot-test를 해봅시다!!

snapshot test

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

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

실제 기능은 34-02-jest-unit-test 폴더 의 파일을 복사해서 사용하고, 우리는 테스트코드만 바꿔주도록 하겠습니다. __test__ 폴더 내부에 34-03-unit-test-snapshot 폴더 를 만들어주었습니다.
그리고 똑같이 index.test.tsx 파일을 만들었습니다.

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

❗️result.container 에서 container는 우리가 배웠던 디자인 패턴의 container와는 다른 것 입니다.

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

container 테스트코드 작성해보기

지금까지는 UI에 문제가 없는지 확인하는 테스트를 해봤고 다음은 기능으로 넘어가 기능을 테스트하는 방법을 알아보도록 하겠습니다.

기능 테스트코드는 버튼을 눌렀을 때 제대로 작동하는지를 테스트 하겠습니다. 우선은 실제기능을 담은 폴더를 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 요청 테스트 코드 작성하기

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

그럼 어떻게 테스트 코드를 작성하게 될까요?
바로 mocking 을 이용해 만들게 되는데요, mocking 이란 요청과 응답을 가짜로 생성하는 것을 뜻합니다. 즉, 백엔드 없이 프론트에서만 테스트를 하는 것 입니다.

mocking으로 테스트 코드 만들기

mocking은 결국 api를 가짜로 만들어야 하기때문에 각각 라이브러리마다 제공해주는 기능이 있습니다.

yarn add -D msw cross-fetch next-router-mock
차례대로 설치해주기!

❗️04-04-graphql-mutation-input 파일을 복사했고, 바뀐 백엔드 주소에 해당하는 쿼리문의 변수부분, variables를 변경했습니다.

//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는 테스트 코드를 먼저 작성하고 그 후에 테스트를 통과하기 위한 최소한의 실제 코드를 작성합니다. 그 다음에 코드를 리팩토링합니다.



우리 지금 디펜던시에 다운이 되었는데 데브디펜던스에 있어야 한다.
실행되는데는 문제가 없다

폴더명은 이렇게 ,

dlfjg이렇게 하고 yarn test 하면 저 폴더 실행


yarn add -g jest
yarn add jest

text next.js만 테스트 폴더를 뺴놓는다. 페이지로 분리되지 않게 하기 위함



똑같이 만들기

브라우저 자체 내장기능 -> fetch.

이거는 브라우저에서 그려지는게 아니라서 ,, 패치라는 라이브러리를 다운받자
크로스패치!qmfkdn브라우저는 엑시오스 있었는데 지금 없으니까 엑시오스같은거 설치


최종 설명!
버튼을 클릭했을때 api 요청이 있는 경우가 있다
항상 백엔드가 열려있을수 없고, 디비까지 가고 하여트 ㅣㅇ러 시간들때문에
가짜 api(mock)를 만들어서 사용한다. -> 모킹

src > commons에 그랲큐엘 만들었다. 여기서 그려진 코드는 응답을 보내주는 코드이다. 엔드포인트도 적어줬다!

yarn test 가 실행될 때 실행되어야 하는데, jest.setup.js 만들어서 모든 테스트 코드가 먼저 실행되고, 다른 코드가 끝나면 테스트 코드가 끝나는 것을 만들었고jest.config다시 설정해줬다.

아폴로 세팅을 index.test.tsx 해주었고 목 에이피아이랑 똑같이 설정해줬다. 앤드포인트는 우리가 적어준 주소로 (가짜주소) 간다. 실전에서 , 테스트에서 사용되는 코드는 바뀌면 안된다. 추가된게 있다면은 테스트코드에서 가져올 수 있게 role 써줬다.

src에 id를 큐큐큐로 하드코딩 해서 실제 중간 페이지에 boardId는 qqq 가 들어간다. 테스트 페이지에서 라우터도 가짜로, mockRouter 로 이동했고 예상한다고 맞으면 맞다고 뜨고 아니면 실패라고 뜬다.


페이지 이동 시나리오! 사이프레스를 통해서 아까 그 크롬에 접속한다.

싸이프레스는 그냥 실제로 유저가 한번 해보는 것 같이 그려보는것이다.

profile
Slow and steady wins the race.

0개의 댓글