나는 강의를 들으며 강의 내용을 내가 진행한 프로젝트에 적용해보기로 하였다.
해당 프로젝트는 Next를 활용해서 구현하였고, 적용을 위해 추가적인 세팅이 필요했다.
jest.config.ts
import type { Config } from "jest";
import nextJest from "next/jest";
const createJestConfig = nextJest({
dir: "./", // jest가 동작되는 기본 경로 설정
});
const config: Config = {
preset: "ts-jest", // jest 설정에 기반이 되는 preset 등록
coverageProvider: "v8", // coverage 코드 추적을 위해 사용되는 provider 설정
testEnvironment: "jsdom", // test를 위해 사용되는 환경 등록 (web app 기반을 개발한 경우 jsdom 활용)
setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"], // jest.setup 환경 등록
// module들을 다른 resource로 대체하여 사용되고자 할 때 설정
moduleNameMapper: {
"^@/(.*)$": "<rootDir>/app/$1",
},
};
export default createJestConfig(config);
위와 같이 jest.config.ts 파일을 작성해주었다.
나는 프로젝트에서
"baseUrl": ".",
"paths": {
"@/*": ["./app/*"]
}
위와 같은 절대 경로를 사용하고 있으므로 moduleNameMapper 속성을 통해 추가 설정이 필요했다.
jest.setup.ts
import "@testing-library/jest-dom";
jest.setup.ts 파일을 생성해 준 뒤 라이브러리를 import 해주었다.
jest.setup.ts 파일에 import "@testing-library/jest-dom"을 추가하는 것은 Jest 테스트 환경에서 @testing-library/jest-dom 라이브러리의 커스텀 매처(custom matcher)들을 사용할 수 있게 해준다고 한다.
__mocks__ 폴더
회원가입 페이지에서 next가 제공하는 next/navigation을 사용하고 있다.
jest 환경에서 회원가입 페이지를 불러오면 이 때문에 에러가 났다.
이를 해결하기 위해 __mock__/next/navigation.ts 파일을 만든 후
export const useRouter = jest.fn();
위처럼 mocking 해주면 주면 에러가 사라지게 된다.
mock에 대해서는 더 알아봐야 하지만, 우선 에러가 사라졌으니 일단 넘어가자!!
내 프로젝트에서는 위와 같은 회원가입 페이지가 있다.
Test 1: 이메일, 비밀번호, 닉네임을 모두 입력하면 가입하기 버튼이 활성화된다.
describe("회원가입 테스트", () => {
test("이메일, 비밀번호, 닉네임을 모두 입력하면 가입하기 버튼이 활성화된다.", async () => {
//given: 회원가입 페이지가 그려짐
render(<RegisterPage />);
const registerButton = screen.getByRole("button", { name: "가입하기" });
expect(registerButton).toBeDisabled();
//when: 모든 input이 입력됨
const idInput = screen.getByLabelText("아이디 (이메일)");
const passwordInput = screen.getByLabelText("비밀번호 (8~16자)");
const nicknameInput = screen.getByLabelText("닉네임");
fireEvent.change(idInput, { target: { value: "brian990614@naver.com" } });
fireEvent.change(passwordInput, { target: { value: "Gusals990!" } });
fireEvent.change(nicknameInput, { target: { value: "brian" } });
//then: 가입하기 버튼이 활성화됨
expect(registerButton).toBeEnabled();
});
});
우선 만들어놓은 RegisterPage를 render를 통해 불러왔다.
<Button disabled={ableRegister} className="w-full">
가입하기
</Button>
위는 회원가입 페이지에 있는 가입하기 버튼이다.
아무것도 하지 않으면 디폴트로 disabled={true} 로 상태로 되어있다.
이를 getByRole로 해당 버튼을 찾은 뒤 expect & toBeDisabled의 조합으로 해당 버튼이 비활성화 되어 있는지 체크하였다.
이제 각각의 input을 불러와야 한다.
<label htmlFor={field} className="text-text-gray-6 body2-semibold">
{label}
</label>
<input
id={field}
type={type}
value={text}
className="w-full py-[0.6rem] pl-[0.4rem] border-b-[0.1rem] border-text-gray-6 mt-[0.9rem] body2-medium"
onChange={handleInputChange}
/>
위 코드는 회원가입 페이지에 있는 label & input의 조합으로 이루어진 FormInput 컴포넌트의 일부분이다.
우선 label의 htmlFor 속성과 input의 id 속성 값을 일치시켜준 뒤, label의 값(위 코드에서는{label})을 불러오면 해당 label과 연결된 input을 불러올 수 있다.
<FormInput
label="아이디 (이메일)"
field="email"
errorMsg={getErrorMessage("email")}
onChange={handlers.changeEmail}
className="mb-[3.2rem]"
/>
회원가입 페이지에서 FormInput 컴포넌트를 사용하는 코드 중 일부이다.
즉 외부에서 전달한 field값을 각각 label과 input에 부여해서 연결시켜주고, 전달한 label 값을 label 태그 값으로 설정한 뒤 해당 label 값을 통해 테스트코드에서 관련 input을 불러올 수 있게 된다.
getByLabelText를 통해 각 input을 불러올 수 있었다.
그 후 fireEvent 객체에 있는 change 메소드를 통해 각각의 input에 value를 설정해 주었다.
그러면 가입하기 버튼이 활성화되어야 하므로 expect & toBeEnabled의 조합으로 해당 버튼이 활성화 되는지 체크하였다.
이제 한번 테스트를 돌려보자.
위와 같이 관련 테스트가 무사히 통과하는 것을 확인할 수 있다.
Test 2: 이메일 형식에 맞지 않는 이메일을 입력한 뒤 가입하기 버튼을 누르면 이메일 관련 에러메시지가 뜬다.
위와 같이 이메일 형식에 맞지 않는 아이디로 회원가입을 시도하면 에러메시지가 뜨게 된다.
이를 테스트하기 위해 describe 안에 test를 추가해주었다.
test("이메일 형식에 맞지 않는 이메일을 입력한 뒤 가입하기 버튼을 누르면 이메일 관련 에러메시지가 뜬다.", () => {
render(<RegisterPage />);
const registerButton = screen.getByRole("button", { name: "가입하기" });
expect(registerButton).toBeDisabled();
//when: 이메일 형식에 맞지 않는 아이디가 입력됨
const idInput = screen.getByLabelText("아이디 (이메일)");
const passwordInput = screen.getByLabelText("비밀번호 (8~16자)");
const nicknameInput = screen.getByLabelText("닉네임");
fireEvent.change(idInput, { target: { value: "brian990614" } });
fireEvent.change(passwordInput, { target: { value: "Gusals990!" } });
fireEvent.change(nicknameInput, { target: { value: "brian" } });
//then: 이메일 관련 에러메시지가 띄워짐
expect(registerButton).toBeEnabled();
fireEvent.click(registerButton);
const emailErrorMessage =
screen.getByText("이메일 형식이 올바르지 않습니다.");
const inputErrorMessage = screen.getByTestId("email-error");
expect(emailErrorMessage).toBeInTheDocument();
expect(inputErrorMessage).toBeInTheDocument();
//when: 이메일 형식에 맞는 아이디로 다시 입력되면
fireEvent.change(idInput, { target: { value: "brian990614@naver.com" } });
//then:에러 메시지가 사라짐
expect(emailErrorMessage).not.toBeInTheDocument();
});
이전 테스트와 겹치는 부분들이 상당히 많다.
우선 모든 input들을 불러온 뒤 각각 value를 채워준다.
이 때 이메일 형식에 필수적인 '@**.com' 형식을 제외한 값을 아이디 input value에 부여해주었다.
그 후 fireEvent 객체에 있는 click 메소드를 통해 가입하기 버튼을 클릭했다.
이 때 이메일 형식에 맞는 아이디가 아니므로 에러 메시지가 출력되어야 한다.
<FormErrorMsg testId={`${field}-error`} errorMsg={errorMsg} />
FormInput 컴포넌트에는 위와 같은 코드가 포함되어있다.
export default function FormErrorMsg({ testId, errorMsg }: FormErrorMsgProps) {
return (
errorMsg && (
<p data-testid={testId} className="body3-semibold text-error mt-[0.8rem]">
{errorMsg}
</p>
)
);
}
FormErrorMsg 컴포넌트는 위와 같이 구현되어있다.
보면 위 p 태그에 data-testid를 부여했다.
해당 testid를 통해 테스트 환경에서 해당 태그를 불러올 수 있다.
테스트코드에서는 getByTestId를 통해 아이디 에러 메시지 관련 태그를 불러왔다.
에러 메시지가 뜨는지와 더불어 에러 메시지 내용도 알맞게 뜨는지 확인하기 위해 getByText를 사용하여 알맞은 에러 메시지 텍스트가 뜨는지도 확인하였다.
에러 이후 이메일 형식에 맞는 아이디를 제대로 입력하면 에러 메시지가 사라지게 된다.
fireEvent 객체의 change를 활용하여 아이디 input의 value를 바꿔준 뒤 .not.toBeInTheDocument()를 통해 에러 메시지가 뜨지 않는지도 확인해 주었다.
한번 테스트를 돌려보자.
위와 같이 테스트가 잘 통과되는 것을 확인할 수 있다!
Test 3: 비밀번호 형식에 맞지 않는 비밀번호를 입력한 뒤 가입하기 버튼을 누르면 비밀번호 관련 에러메시지가 뜬다.
아이디(이메일) 과 비슷하게 비밀번호도 특정 형식을 만족해야 했다.
아이디와는 달리 비밀번호는 두가지 형식을 모두 만족하는지 체크하고 있다.
1. 비밀번호는 영어, 숫자, 특수문자가 한개이상 포함되어야 한다.
비밀번호가 만족해야 하는 첫번째 형식이다.
위와 같이 조건을 만족하지 못한 채 가입하기 버튼을 누르면 비밀번호 input 아래에 에러 메시지가 나타나게 된다.
1. 비밀번호는 8~16 자리 여야 한다.
위와 같이 비밀번호가 8자리수 미만이거나 혹은 16자리 수를 넘어가면 위와 같이 에러 메시지가 뜨게 된다.
테스트코드를 통해 위의 경우 두가지를 모두 테스트해주기로 했다.
test("비밀번호 형식에 맞지 않는 비밀번호를 입력한 뒤 가입하기 버튼을 누르면 비밀번호 관련 에러메시지가 뜬다.", () => {
render(<RegisterPage />);
const registerButton = screen.getByRole("button", { name: "가입하기" });
expect(registerButton).toBeDisabled();
//when: 비밀번호 형식(영어, 숫자, 특수기호 중 하나라도 포함되지 않는 비밀번호)에 맞지 않는 비밀번호가 입력됨
const idInput = screen.getByLabelText("아이디 (이메일)");
const passwordInput = screen.getByLabelText("비밀번호 (8~16자)");
const nicknameInput = screen.getByLabelText("닉네임");
fireEvent.change(idInput, { target: { value: "brian990614@naver.com" } });
fireEvent.change(passwordInput, { target: { value: "gusals990" } });
fireEvent.change(nicknameInput, { target: { value: "brian" } });
expect(registerButton).toBeEnabled();
fireEvent.click(registerButton);
//then: 비밀번호 관련 에러메시지1이 띄워짐
const passwordErrorMessage1 = screen.getByText(
"영문 대소문자, 숫자, 특수문자를 1개 이상 포함해야 합니다."
);
const inputErrorMessage = screen.getByTestId("password-error");
expect(inputErrorMessage).toBeInTheDocument();
expect(passwordErrorMessage1).toBeInTheDocument();
//when: 비밀번호 형식(8~16 자리수)에 맞지 않는 비밀번호가 입력됨
fireEvent.change(passwordInput, { target: { value: "gus99^^" } });
//then: 비밀번호 관련 에러메시지2가 띄워짐
const passwordErrorMessage2 = screen.getByText(
"비밀번호는 8~16 자리수여야 합니다."
);
expect(inputErrorMessage).toBeInTheDocument();
expect(passwordErrorMessage2).toBeInTheDocument();
//when: 모든 조건을 만족하는 비밀번호가 입력됨
fireEvent.change(passwordInput, { target: { value: "gusals990^^" } });
//then: 비밀번호 관련 에러메시지가 사라짐
expect(inputErrorMessage).not.toBeInTheDocument();
});
방식은 위의 경우와 똑같다.
fireEvent 객체의 change를 통해 각각 상황에 대해 테스트를 진행하였다.
테스트를 한번 돌려보자.
위와 같이 모든 테스트가 잘 통과하는 것을 확인할 수 있었다!!
render(<RegisterPage />);
회원가입 페이지를 불러오는 로직은 모든 테스트에서 초기에 필요하므로 beforeEach 메소드를 활용하여 처리할 수 있을 것 같다.
지금까지 처리한 로컬 회원가입 관련 최종 테스트코드는 다음과 같다.
import RegisterPage from "../app/(pages)/(auth)/register/page";
import "@testing-library/jest-dom";
import { fireEvent, render, screen } from "@testing-library/react";
describe("회원가입 테스트", () => {
beforeEach(() => {
//given: 회원가입 페이지가 그려짐
render(<RegisterPage />);
});
test("이메일, 비밀번호, 닉네임을 모두 입력하면 가입하기 버튼이 활성화된다.", async () => {
const registerButton = screen.getByRole("button", { name: "가입하기" });
expect(registerButton).toBeDisabled();
//when: 모든 input이 입력됨
const idInput = screen.getByLabelText("아이디 (이메일)");
const passwordInput = screen.getByLabelText("비밀번호 (8~16자)");
const nicknameInput = screen.getByLabelText("닉네임");
fireEvent.change(idInput, { target: { value: "brian990614@naver.com" } });
fireEvent.change(passwordInput, { target: { value: "Gusals990!" } });
fireEvent.change(nicknameInput, { target: { value: "brian" } });
//then: 가입하기 버튼이 활성화됨
expect(registerButton).toBeEnabled();
});
test("이메일 형식에 맞지 않는 이메일을 입력한 뒤 가입하기 버튼을 누르면 이메일 관련 에러메시지가 뜬다.", () => {
const registerButton = screen.getByRole("button", { name: "가입하기" });
expect(registerButton).toBeDisabled();
//when: 이메일 형식에 맞지 않는 아이디가 입력됨
const idInput = screen.getByLabelText("아이디 (이메일)");
const passwordInput = screen.getByLabelText("비밀번호 (8~16자)");
const nicknameInput = screen.getByLabelText("닉네임");
fireEvent.change(idInput, { target: { value: "brian990614" } });
fireEvent.change(passwordInput, { target: { value: "Gusals990!" } });
fireEvent.change(nicknameInput, { target: { value: "brian" } });
//then: 이메일 관련 에러메시지가 띄워짐
expect(registerButton).toBeEnabled();
fireEvent.click(registerButton);
const emailErrorMessage =
screen.getByText("이메일 형식이 올바르지 않습니다.");
const inputErrorMessage = screen.getByTestId("email-error");
expect(emailErrorMessage).toBeInTheDocument();
expect(inputErrorMessage).toBeInTheDocument();
//when: 이메일 형식에 맞는 아이디로 다시 입력되면
fireEvent.change(idInput, { target: { value: "brian990614@naver.com" } });
//then:에러 메시지가 사라짐
expect(emailErrorMessage).not.toBeInTheDocument();
});
test("비밀번호 형식에 맞지 않는 비밀번호를 입력한 뒤 가입하기 버튼을 누르면 비밀번호 관련 에러메시지가 뜬다.", () => {
const registerButton = screen.getByRole("button", { name: "가입하기" });
expect(registerButton).toBeDisabled();
//when: 비밀번호 형식(영어, 숫자, 특수기호 중 하나라도 포함되지 않는 비밀번호)에 맞지 않는 비밀번호가 입력됨
const idInput = screen.getByLabelText("아이디 (이메일)");
const passwordInput = screen.getByLabelText("비밀번호 (8~16자)");
const nicknameInput = screen.getByLabelText("닉네임");
fireEvent.change(idInput, { target: { value: "brian990614@naver.com" } });
fireEvent.change(passwordInput, { target: { value: "gusals990" } });
fireEvent.change(nicknameInput, { target: { value: "brian" } });
expect(registerButton).toBeEnabled();
fireEvent.click(registerButton);
//then: 비밀번호 관련 에러메시지1이 띄워짐
const passwordErrorMessage1 = screen.getByText(
"영문 대소문자, 숫자, 특수문자를 1개 이상 포함해야 합니다."
);
const inputErrorMessage = screen.getByTestId("password-error");
expect(inputErrorMessage).toBeInTheDocument();
expect(passwordErrorMessage1).toBeInTheDocument();
//when: 비밀번호 형식(8~16 자리수)에 맞지 않는 비밀번호가 입력됨
fireEvent.change(passwordInput, { target: { value: "gus99^^" } });
//then: 비밀번호 관련 에러메시지2가 띄워짐
const passwordErrorMessage2 = screen.getByText(
"비밀번호는 8~16 자리수여야 합니다."
);
expect(inputErrorMessage).toBeInTheDocument();
expect(passwordErrorMessage2).toBeInTheDocument();
//when: 모든 조건을 만족하는 비밀번호가 입력됨
fireEvent.change(passwordInput, { target: { value: "gusals990^^" } });
//then: 비밀번호 관련 에러메시지가 사라짐
expect(inputErrorMessage).not.toBeInTheDocument();
});
});
'FrontEnd > Test Code' 카테고리의 다른 글
현업에서의 테스트코드 (0) | 2024.07.17 |
---|---|
Cypress로 E2E 테스트 진행하기 (0) | 2024.07.02 |
프론트엔드 테스트에서 http통신 mocking 하기(Feat: MSW) (2) | 2024.06.27 |
테스트 코드 문법 익히기 (Feat: Jest) (2) | 2024.06.21 |
테스트 코드 이론 (0) | 2024.06.19 |