describe("회원가입테스트", () => {
test("테스트케이스작성", () => {
//given
//when
//then
});
});
한 테스트코드 내에서 given
when
then
으로 나눠서 작성하면 테스트의 가독성과 이해도를 높이고, 명확한 테스트 흐름을 유지하기 좋다.
given
준비단계
when
실행단계
then
검증단계
실패케이스를 먼저적냐, 성공케이스를 먼저적냐 순서에 대해 고민하기 시작했다.
뭐가맞는걸까.....
일단 지금의 내 생각엔 테스트코드를 적는 이유가 오류를 찾아내기 위해서 적는것이 큰 목적이다 보니 우선 실패케이스를 먼저적고, 성공케이스를 나중에 적기로했다.
위의 구조를 참고하여 먼저 given
when
then
단계에서 무엇을 할지 정한 후 실패시 테스트케이스를 작성했다.
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import "@testing-library/jest-dom"; // jest로 테스트할때는 테스트파일에 이부분 꼭 import 해야한다.
import { fireEvent, render, screen } from "@testing-library/react";
import SignupPage from "../pages/SignupPage";
import { createMemoryRouter, RouterProvider } from "react-router-dom";
const queryClient = new QueryClient({
defaultOptions: {},
});
describe("회원가입 테스트", () => {
test("비밀번호와 비밀번호 확인 값이 일치하지 않으면 에러메세지가 표시된다.", async () => {
// 1. given 준비단계 - 회원가입 페이지가 그려짐
// 회원가입이 react-router-dom(useNavigate)나 react-query등을 사용하므로 provider로 감싸줘야한다.
const routes = [{ path: "/signup", element: <SignupPage /> }];
const router = createMemoryRouter(routes, {
initialEntries: ["/signup"],
initialIndex: 0,
});
render(
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>
);
// 2. when 실행단계 - 비밀번호롸 비밀번호 확인 값이 일치하지 않음
// 라벨값으로 input을 가져옴
const passwordInput = screen.getByLabelText("비밀번호");
const confirmPasswordInput = screen.getByLabelText("비밀번호 확인");
// 테스트케이스 실패를 위해, 일부러 password와 wrongPassword에 다른값을 넣어줌
fireEvent.change(passwordInput, { target: { value: "password" } });
fireEvent.change(confirmPasswordInput, {
target: { value: "wrongPassword" },
});
// 3. then 검증단계 - 에러메세지가 표시됨
// id가 error-message인 값을 찾아 존재하는지 확인, 에러메세지는 기다렸다가 실패시 띄워야하기때문에 await
const errorMessage = await screen.findByTestId("error-message");
expect(errorMessage).toBeInTheDocument();
});
});
test("이메일을 입력하고, 비밀번호와 비밀번호 확인값이 일치하면 회원가입 버튼이 활성화된다", () => {
// 1. given : 회원가입페이지가 그려짐 위와동일. => beforeEach로 대체
const routes = [{ path: "/signup", element: <SignupPage /> }];
const router = createMemoryRouter(routes, {
initialEntries: ["/signup"],
initialIndex: 0,
});
render(
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>
);
// 현재 버튼이 비활성화 되어있는지 테스트
const signupButton = screen.getByRole("button", { name: "회원가입" });
expect(signupButton).toBeDisabled();
// 2. when : 이메일 입력, 비밀번호 비밀번호확인 일치
const emailInput = screen.getByLabelText("이메일");
const passwordInput = screen.getByLabelText("비밀번호");
const confirmPasswordInput = screen.getByLabelText("비밀번호 확인");
// 테스트케이스 성공을위해, password와 wrongPassword에 같은 값을 넣어줌
fireEvent.change(emailInput, {
target: { value: "button-active@gmail.com" },
});
fireEvent.change(passwordInput, { target: { value: "password" } });
fireEvent.change(confirmPasswordInput, {
target: { value: "password" },
});
// 3. then : 회원가입 버튼 활성화
expect(signupButton).toBeEnabled();
});
실패케이스와 성공케이스를 따로따로 test로 구현을 하다보니, 처음 동일한 구문을 돌때 똑같은 코드를 또 입력하기가 가독성이 떨어졌다.
이럴때 이론에서 배웠던 beforeEach
를 사용하는것이다.
beforeEach
는 각각의 test구문을 각각 먼저 실행되는 코드, beforeAll
은 모든 test구문 실행 전 한번만 실행되는 코드.
따라서 이런경우는 beforeEach
가 실행되어야한다.
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import "@testing-library/jest-dom"; // jest로 테스트할때는 테스트파일에 이부분 꼭 import 해야한다.
import { fireEvent, render, screen } from "@testing-library/react";
import SignupPage from "../pages/SignupPage";
import { createMemoryRouter, RouterProvider } from "react-router-dom";
const queryClient = new QueryClient({
defaultOptions: {},
});
describe("회원가입 테스트", () => {
test("비밀번호와 비밀번호 확인 값이 일치하지 않으면 에러메세지가 표시된다.", async () => {
// 1. given : 회원가입 페이지가 그려짐
const routes = [{ path: "/signup", element: <SignupPage /> }];
const router = createMemoryRouter(routes, {
initialEntries: ["/signup"],
initialIndex: 0,
});
render(
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>
);
// 2. when 실행단계 - 비밀번호롸 비밀번호 확인 값이 일치하지 않음
...
});
test("이메일을 입력하고, 비밀번호와 비밀번호 확인값이 일치하면 회원가입 버튼이 활성화된다", () => {
// 1. given : 회원가입페이지가 그려짐 위와동일.
// 2. when : 이메일을 입력하고,
...
});
});
-----------------------------
위 구문을 아래처럼 변경할 수 있다.
-----------------------------
describe("회원가입 테스트", () => {
beforeEach(() => {
const routes = [{ path: "/signup", element: <SignupPage /> }];
const router = createMemoryRouter(routes, {
initialEntries: ["/signup"],
initialIndex: 0,
});
render(
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>
);
});
test("비밀번호와 비밀번호 확인 값이 일치하지 않으면 에러메세지가 표시된다.", async () => {
// 1. given : 회원가입 페이지가 그려짐 => beforeEach로 대체
// 2. when 실행단계 - 비밀번호롸 비밀번호 확인 값이 일치하지 않음
...
});
test("이메일을 입력하고, 비밀번호와 비밀번호 확인값이 일치하면 회원가입 버튼이 활성화된다", () => {
// 1. given : 회원가입페이지가 그려짐 위와동일. => beforeEach로 대체
// 2. when : 이메일을 입력하고,
...
});
});
지금까지는 api통신이 없는 부분만 구현했지만, 테스트케이스를 적다보니 페이지당의 통합테스트 부분에서 api통신시 에러가나면? 에대한 고찰이 시작됐다.
테스트 케이스 시 에러가 나면 테스트케이스는 성공하나 콘솔에 빨간글씨로 오류처럼 떠서 마치 테스트케이스가 실패한것처럼 보여준다.
react-query 공식문서에서는 아래처럼 터미널에서 꺼주라고 되어있지만, 테스트코드에서 버튼을 클릭하는데, 실제 서버에 요청이 들어간다
https://tanstack.com/query/v4/docs/framework/react/guides/testing
const queryClient = new QueryClient({
defaultOptions: {},
// 터미널에서 400에러를 꺼줌 => react-query에서 권장하는방법 : 실제 서버에 요청이 들어감
logger: {
log: console.log,
warn: console.warn,
error: process.env.NODE_ENV === "test" ? () => {} : console.error,
},
});
예를들면 로그인을 구현하는데 이메일과 비밀번호를 넣었을때 실제값과 맞는지를 검증하려면 결국 서버에 직접 요청을 해야하는건데 그건 너무 과소비같았다.
처음에는 이메일과 비밀번호를 각 input에적고 로그인버튼클릭 까지 테스트코드에 넣으니 실제 서버로 http통신이 가는것이였다.
그래서 가짜 통신을 만들어주면 좋겠다는 생각을했고 찾아보았다.
nock
이라는 패키지를 설치하면 가짜 request를 보내줄 수 있어, 실제 서버에 요청이 안가 부담이 없다는 장점이 있다.
npm install --save-dev nock
으로 nock
을 설치해줬다. 참고사이트 https://github.com/nock/nock
beforeEach(() => {
jest.spyOn(console, "error").mockImplementation(() => {});
});
afterAll(() => {
jest.restoreAllMocks();
});
nock("https://inflearn.byeongjinkang.com") // 서버에 요청하지않고 nock 패키지로 bad request를 만듬
.post("/user/login/", {
username: "wrong@email.com",
password: "wrongPassword",
})
.reply(400, { id: "NO_SEARCH_USER" });
최종코드
const queryClient = new QueryClient({
defaultOptions: {},
// 터미널에서 400에러를 꺼줌 => react-query에서 권장하는방법 https://tanstack.com/query/v4/docs/framework/react/guides/testing
// logger: {
// log: console.log,
// warn: console.warn,
// error: process.env.NODE_ENV === "test" ? () => {} : console.error,
// },
});
describe("로그인 테스트", () => {
/*
mocking을 이용해 400에러를 좀 더 확실하게 직관적으로 꺼줌
에러있을때는 아무것도 실행하지말아라, 그리고 테스트케이스 다 돌면 원상복구해라
하지만 이 방법의 문제점은 실제 서버에 request가 들어간다.
따라서, http request를 mocking하는 방법으로 개선할수있다.
이는 서버에서 bad request 온것처럼 구현하는것인데 nock이라는 패키지를 설치해서 구현할 수 있다. '$ npm install --save-dev nock' 참고문서: https://github.com/nock/nock
*/
beforeEach(() => {
jest.spyOn(console, "error").mockImplementation(() => {});
});
afterAll(() => {
jest.restoreAllMocks();
});
test("로그인에 실패하면 에러메세지가 나타난다.", async () => {
// given - 로그인 페이지가 그려짐
const routes = [{ path: "/signup", element: <LoginPage /> }];
const router = createMemoryRouter(routes, {
initialEntries: ["/signup"],
initialIndex: 0,
});
render(
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>
);
// when - 사용자가 로그인에 실패함
nock("https://inflearn.byeongjinkang.com") // 서버에 요청하지않고 nock 패키지로 bad request를 만듬
.post("/user/login/", {
username: "wrong@email.com",
password: "wrongPassword",
})
.reply(400, { id: "NO_SEARCH_USER" });
const wrapper = ({ children }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
const { result } = renderHook(() => useLogin(), { wrapper });
/*
에러메세지 검증할떄 실제 http call이 안되서 테스트케이스 오류가 발생한다.
이메일과 비밀번호에 잘못된값을 넣고 로그인버튼을 눌러보면 테스트케이스는 성공했고 400에러가뜬다.
*/
const emailInput = screen.getByLabelText("이메일");
const passwordInput = screen.getByLabelText("비밀번호");
fireEvent.change(emailInput, { target: { value: "wrong@email.com" } });
fireEvent.change(passwordInput, { target: { value: "wrongPassword" } });
const loginButton = screen.getByRole("button", { name: "로그인" });
fireEvent.click(loginButton);
// then - 로그인 에러 메세지가 화면에 나타남
await waitFor(() => result.current.isError);
// 에러메세지검증
const errorMessage = await screen.findByTestId("error-message");
expect(errorMessage).toBeInTheDocument();
});
});