[32-1] 테스트 코드
[32-2] 일반적인 단위테스트
마우스로 클릭을 통해 api를 요청하는 작업같은 것들을 대신해주는 것 이다.
📂 테스트코드의 필요성
💡 언제부터 테스트코드를 작성할까?
스타트업
: 버전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("나만의 테스트 그룹만들기",()=>{ 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요청 테스트 코드
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