마우스로 클릭을 통해 api를 요청하는 작업같은 것들을 대신해주는 것
몇달간 개발한 사이트의 1차개발이 완료된 후,
2주 정도 버그여부를 확인 및 수정하여 테스트를 가진다.
버그를 열심히 잡은 결과 깔끔한 어플리케이션이 되었다고 했을 때 개발자들은 배포를 한다.그리고 2차개발에 돌입해 위와 같은 과정으로 기능을 하나 더 만들고 배포 3일 후,
갑자기 멀쩡하던 다른 기능에서 에러가 발생하기 시작한다.
분명 버그를 다 잡고 배포를 했는데 왜 에러를 보게 되었을까?2차 배포, 다시 말해 업데이트 배포한 코드들이 이전 배포한 기능에 영향을 주고 있는 것 이다.
예를 들어 업데이트 배포시의 결제기능과 1차 배포시의 상품관련 기능에 연관된 코드를 가지고 있어 에러를 보게 되는 것이다.(결제추가하면서 꼬이게 된것!)
그럼 과연 하나의 기능에만 영향을 미친다고 확신할 수 있을까?
만약 아니게 된다면 버전1 부터 길고 긴 버그 수정의 기간을 가져야 한다.
이런 힘든 버그잡는 과정을 테스트코드는 조금 더 수월하도록 도와주고있다.
서비스의 사이즈가 커질수록 테스트 코드의 유용함이 커지며, 버그 수정의 과정이 편리해진다.
❗️ 그럼 언제부터 테스트코드를 작성할까?
스타트업을 기준으로, 버전1의 배포가 끝난 후 만드는것이 일반적으로 가장 적절하다.
버전1의 개발 시점에서는 테스트코드보다 런칭에 조금 더 초점이 맞춰지기 때문에 배포후에 만드는 것이 비지니스 적으로 가장 적절한 때임!하지만, 회사마다 다르기 때문에 무조건적인것은 아니다~~
단위 테스트
단위 테스트는 버튼 클릭과 같은 기능 하나하나를 테스트한다(jest를 사용)
통합테스트 - 사용 프레임 워크 : jest
여러 기능을 한꺼번에 테스트(jest를 사용)
E2E(End To End) 테스트
로그인 후 결제를 하고 환불할 때 같은 시나리오가 있는 테스트를 할 때 사용한다.
E2E 테스트를 진행할때는 가상의 브라우저를 띄워 테스트를 진행한다.
테스트 코드를 만들 수 있도록 도와주는 프레임워크는 여러가지가 있지만,
그 중에 Jest를 사용하여 개별 단위테스코드를 만들어보자.
✅ jset를 사용하기 위한 기본적인 세팅
- 설치명령어
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 함께 사용하기
.eslintrc.js 파일로 들어가 아래 내용으로 바꿔주기plugins: ["react","jest/globals"],
4.package.json에 jest를 실행시키기 위한 명령어 추가하기
"scripts": { "test": "jest", "test:watch": "jest --watch" }
test:watch는 소스코드를 고칠때마다 jest가 실행되길 원하신다면 추가해주면 된다.
만일 추가하지 않는다면, 원할때마다
yarn test
를 입력해서 jest를 실행해야 한다.
✅ 테스트 코드 작성
- section33 / 33-01-jest / index.ts
// index.ts 파일 -> 실제 기능 export const add = (a: number, b: number) => { return a + b; };
React에서는
33-01-jest폴더
에__test__ 폴더
를 생성해index.test.ts
파일을 만들어줘야 하지만,
Next에서는__test__ 폴더
를33-01-jest폴더
에 넣지않고 pages 바깥에 위치시킨다.왜냐하면, Next에서 pages안의 폴더는 페이지가 되기 때문!!!
따라서 우리는 pages 폴더 바깥쪽에
__test__ 폴더
를 생성하고,
해당폴더 안쪽에33-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가 들어간 모든 파일을 찾아 테스트를 진행한다.
✅ 나만의 테스트 그룹 만들기
만일 위와 같은 테스트를 한번에 여러개를 하고싶다 하시면, 테스트 그룹을 만들어주면 된다.
테스트 그룹을 만들때는 아래와 같이
describe
를 이용해서 만들 수 있다.describe("나만의 테스트 그룹", () => { it("더하기 테스트", () => { const result = add(3, 5); expect(result).toBe(8); }); it("빼기 테스트", () => { const result = add(3, 5); expect(result).toBe(8); }); it("곱하기 테스트", () => { const result = add(3, 5); expect(result).toBe(8); }); });
테스트코드는 따로 분리하는 것이 좋다.
일반적으로 프론트엔드에서 단위테스트를 진행할때와 비슷하게 작성해보자
✅ UI(presenter) 테스트코드 작성 해보기
- section33 / 33-02-jest-unit-test / index.ts
export default function JestUnitTestPage(): JSX.Element { return ( <> <div>짱구는 5살</div> 짱구 취미 : <input type="text" /> <button>놀러가기</button> </> ); }
__test__ 폴더
/33-02-jest-unit-test 폴더
/index.test.tsx 파일
실제 기능파일의 확장자와 동일하게 설정할 것!import { render, screen } from "@testing-library/react"; import JestUnitTestPage from "../../pages/section33/33-02-jest-unit-test"; import "@testing-library/jest-dom"; it("내가 원하는 데로 그려지렴", () => { //render:가짜로 그려질 수 있도록 도와주는 도구 render(<JestUnitTestPage />); const myText1 = screen.getByText("짱구는 5살"); expect(myText1).toBeInTheDocument(); const myText2 = screen.getByText("짱구 취미 :"); expect(myText2).toBeInTheDocument(); const myText3 = screen.getByText("놀러가기"); expect(myText3).toBeInTheDocument(); });
컴포넌트가 render가 되고, 렌더링된 결과는 screen에 들어오게 된다.
렌더링 된 결과를 화면에 그려야하는데, 이는 가짜돔인 jest-dom에 그려준다.
이렇게 만든 후
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
스냅샷 테스트는 매번 볼 수 없기 때문에 사진을 찍어둔 후 수정사항이 있을 때, 기존의 사진과 비교해서 다른 부분을 알려준다.
만일 일부러 수정한 부분이라면 다시 사진을 찍어주면 된다.
- pages/section33/33-03-jest-unit-test-snapshot
//기능 실제코드 export default function JestUnitTestSnapShotPage(): JSX.Element { return ( <> <div>짱구는 5살!!!!!!</div> 짱구 취미 : <input type="text" /> <button>놀러가기</button> </> ); }
__test__ 폴더
/33-03-unit-test-snapshot 폴더
/ index.test.tsx//테스트 import { render } from "@testing-library/react"; import "@testing-library/jest-dom"; import JestUnitTestSnapShotPage from "../../pages/section33/33-03-jest-unit-test-snapshot"; it("기존 사진이랑 바뀐게 없는지 비교해보자! - snapshottest", () => { const result = render(<JestUnitTestSnapShotPage />); expect(result.container).toMatchSnapshot(); });
❗️result.container 에서 container는 우리가 배웠던 디자인 패턴의 container와는 다른 것 이다.
이렇게 작성해주시면, 앞으로 snapshot과 비교하게 된다.
만일 이전에 찍어둔 snapshot이 없다면 알아서 찍어주니 걱정하지 않아도 된다.
✅ container 테스트코드 작성해보기
지금까지는 UI에 문제가 없는지 확인하는 테스트를 해봤고 다음은 기능으로 넘어가 기능을 테스트하는 방법을 알아보자!
기능 테스트코드는 버튼을 눌렀을 때 제대로 작동하는지를 테스트 해주면 된다.
우선은 실제기능을 담은 폴더를 pages 폴더 내부에 생성해줘야 한다.
그리고 실제 기능을 담아둘 파일을 생성할때는 확장자를 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}> countUP!! </button> </> ); }
실제 기능에서 해당 기능을 테스트할 때는 어떤 태그가 어떤 일을 하는지 알아야 한다.
따라서
button태그
에role=”count-button”
을 추가해준다.이 기능을 테스트하기 위해
__test__ 폴더
안에 새로운 폴더를 생성하고, 역시 안에 index.test.tsx 파일을 만들어주면 된다.// 테스트 코드 //테스트 import { fireEvent, render, screen } from "@testing-library/react"; import "@testing-library/jest-dom"; import JestUnitTestPage from "../../pages/section33/33-04-jest-unit-test-event"; it("버튼 잘 작동하냐?", () => { const result = render(<JestUnitTestPage />); fireEvent.click(screen.getByRole("count-button")); expect(screen.getByRole("count")).toHaveTextContent("1"); });
이렇게 작성하면, 해당 버튼이 눌렸을 때 제대로 작동하는지 확인할수 있다.
✅ container 내부의 api요청 테스트 코드 작성하기
api요청이 제대로 수행되는지 확인하기 위해서 테스트 코드를 작성하는데, 주의할 점이 있다.
바로 실제 백엔드로 요청을 하는것이 아니라는 점!!
만일 백엔드로 요청을 보내게 되면, 테스트시에 200-300개 정도의 요청을 보내게 될 수 있다.
그럼 서버에 부하가 가기때문에 좋지 못한 방법이다.
그럼 어떻게 테스트 코드를 작성하게 될까?
바로
mocking
을 이용해 만들수 있다.
mocking
이란 요청과 응답을 가짜로 생성하는 것을 뜻한다.즉, 백엔드 없이 프론트에서만 테스트를 하는 것!
- 설치명령어
yarn add --dev msw
yarn add next-router-mock
yarn add --dev cross-fetch
or
yarn add -D msw cross-fetch next-router-mock
//33-05-jest-unit-test-mocking/index.tsx import { useMutation, gql } from "@apollo/client"; import { useRouter } from "next/router"; import { ChangeEvent, useState } from "react"; import { wrapAsync } from "../../../src/commons/libraries/asyncFunc"; const CREATE_BOARD = gql` mutation createBoard($createBoardInput: CreateBoardInput!) { createBoard(createBoardInput: $createBoardInput) { _id writer title contents } } `; export default function GraphqlMutationPage(): JSX.Element { const router = useRouter(); const [writer, setWriter] = useState(""); const [title, setTitle] = useState(""); const [contents, setContents] = useState(""); const [createBoard] = useMutation(CREATE_BOARD); const onClickSubmit = async (): Promise<void> => { const result = await createBoard({ variables: { createBoardInput: { writer, title, contents, password: "1234", }, }, }); console.log(result); const boardId = result.data.createBoard._id; if (typeof boardId === "string") router.push(`/boards/${boardId}`); }; function onChangeWriter(event: ChangeEvent<HTMLInputElement>): void { setWriter(event.target.value); } function onChangeTitle(event: ChangeEvent<HTMLInputElement>): void { setTitle(event.target.value); } function onChangeContents(event: ChangeEvent<HTMLInputElement>): void { setContents(event.target.value); } //한 줄일 때는 (괄호) 필요없음 return ( <div> 작성자: <input role="input-writer" type="text" onChange={onChangeWriter} /> 제목: <input role="input-title" type="text" onChange={onChangeTitle} /> 내용: <input role="input-contents" type="text" onChange={onChangeContents} /> <button role="submit-button" onClick={wrapAsync(onClickSubmit)}> Graphql-API 요청하기 </button> </div> ); }
__test__
/ 33-05-jest-unit-test-mockingimport { fireEvent, render, screen, waitFor } from "@testing-library/react"; import GraphqlMutationPage from "../../pages/section33/33-05-jest-unit-test-mocking"; import mockRouter from "next-router-mock"; import { ApolloClient, ApolloProvider, HttpLink, InMemoryCache, } from "@apollo/client"; import fetch from "cross-fetch"; 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}> <GraphqlMutationPage /> </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"); }); });
가짜 api를 사용하기 위해서 api를 모아둘 수 있는 파일을 새로 만들어준다.
- src/commons/mocks/apis.js
import { graphql } from "msw"; const gql = graphql.link("http://mock.com/graphql"); //백엔드만들기 뭘 응답으로 줄지 만듬 - response정도 export const apis = [ gql.mutation("createBoard", (req, res, ctx) => { //구조분해할당으로 받기 const { writer, title, contents } = req.variables.createBoardInput; // const result = req.variables.createBoardInput; return res( ctx.data({ createBoard: { _id: "qqq", writer, title, contents, __typename: "Board", }, }) ); }), // gql.query("fetchBoard", () => {}), ];
- src/commons/mocks/index.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로 받아오는 결과물이 제대로 된 결과물인지를 검증한다.
- 설치명령어
yarn add --dev cypress
- 33-06-cypress-e2e-test
import { useRouter } from "next/router"; export default function CypressE2EPage(): JSX.Element { const router = useRouter(); const onClickMove = (): void => { void router.push("/section33/33-06-cypress-e2e-test-moved"); }; return <button onClick={onClickMove}>놀러가기</button>; }
- 33-06-cypress-e2e-test-moved
export default function CypressE2EMovedPage(): JSX.Element { return <div>짱구야 놀자!!!!</div>; }
yarn test:e2e
로 확인한다.
테스트 주도 개발을 의미한다.
보통의 개발 과정은 요구 사항을 정의하고, 디자인이 나오고, 이를 토대로 실제 코드를 작성 하고 테스트를 진행한다.
하지만 TDD는 테스트 코드를 먼저 작성하고 그 후에 테스트를 통과하기 위한 최소한의 실제 코드를 작성한다. 그 다음에 코드를 리팩토링한다