테스트코드(jest)
단위테스트 snapshot test
TDD
테스트코드 : 내가 만든 코드를 한번 더 검사하는 코드
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는 테스트 코드를 먼저 작성하고 그 후에 테스트를 통과하기 위한 최소한의 실제 코드를 작성한다. 그 다음에 코드를 리팩토링!
코딩문화!