jest 설치
> npx create-next-app@latest --example with-jest with-jest-app
React Hooks Testing Library 설치
훅을 테스트하기 위하여 설치 필요
jest.setup.ts 파일을 생성하고, 아래와 같이 설정
> import '@testing-library/jest-dom'
test 파일 생성
-watch👉 변경된 파일만 다시 테스트
npx jest --watch
-watchAll👉 모든 테스트를 항상 다시 실행
-watch는 변경된 파일만 돌리지만,-watchAll은 변경 여부 상관없이 전체 테스트를 재실행함.npx jest --watchAll
describe는 테스트 그룹을 묶어주는 역할을 하고, 그 안의 콜백함수 내에 테스트에 쓰일 가짜 변수, 객체들을 선언하여 일회용으로 사용 할 수 있다.toXxx부분에서 사용되는 함수를 흔히Test Mathcher라고 하는데, 위에서 사용된 toEqual() 함수는 값을 비교할때 사용한다.
describe('계산 테스트', () => {
const a = 1, b = 2;
test('a + b는 3이다.', () => {
expect(a + b).toEqual(3);
});
});
/*
describe('그룹 테스트 설명 문자열', () => {
const a = 1, b = 2; // 테스트에 사용할 일회용 가짜 변수 선언
test('개별 테스트 설명 문자열', () => {
expect(검증대상).toXxx(기대결과);
});
});
*/
// 첫번째 파라미터: 작성한 테스트코드가 무엇을 하는지 이름을 정해준다
// 두번째 파라미터: 해당 테스트코드 로직
test('properly adds two numbers', () => {
// expected result
expect(sum(1, 2)).toBe(3);
});
// 혹은, it keyword를 사용한 테스트 코드 작성
it('properly adds two numbers', () => {
expect(sum(1, 2)).toBe(3);
});
// render
test('알맞는 글자를 포함하여 렌더링한다.', () => {
render(<DefaultButton>버튼</DefaultButton>);
const button = screen.getByText('버튼');
expect(button).toBeInTheDocument();
});
it 은 test 로 별칭이 지정되어 it와 동일한 작업을 수행함)@testing-library/react의 render 함수는 해당 컴포넌트를 렌더링해준다.screen.getByTest('버튼')를 통해 "버튼" 문자열을 가지는 컴포넌트를 찾는다.const myBeverage = {
delicious: true,
sour: false,
};
describe('my beverage', () => {
test('is delicious', () => {
expect(myBeverage.delicious).toBeTruthy();
});
test('is not sour', () => {
expect(myBeverage.sour).toBeFalsy();
});
});
it를 하나의 describe아래에 그룹화함.beforeEach/afterEach 훅을 추가할 수 있다.Jest는 다른 방법으로 값을 테스트 하도록 matcher 라는 것을 사용한다.
이는 ‘이거맞니?’ 라고 물어보는 메서드라고 보면 되는데, 기대한 값이 실제 반환된 값과 일치하는 지를 확인하는 작업이다.
// user.js
// 테스트할 함수
function getUser(id) {
return {
id,
email: `user${id}@test.com`,
};
}
module.exports = getUser;
객체가 일치한지 검증
const { getUser } = require('user'); // 테스트할 함수를 가져온다.
test("return a user object", () => {
// getUser(1)의 리턴 결과값이 { 객체 } 값이 같은 경우 true
expect(getUser(1)).toEqual({
id: 1,
email: `user1@test.com`,
});
});
단순히 값 비교
expect(1 + 4).toBe(5);
문자열의 경우에는 단순히 toBe()를 사용해서 문자열이 정확히 일치하는지를 체크하지만,
종종 정규식 기반의 테스트가 필요할 때 toMatch() 함수를 사용하면 된다.
함수가 호출되었는지 여부
test("string", () => {
expect(getUser(1).email).toBe("user1@test.com"); // 단순 문자열 비교
expect(getUser(2).email).toMatch(/.*test.com$/); // 정규식 비교
});
함수가 몇 번 호출되었는지 검증
함수가 지정한 값을 반환하는지 테스트
test('drink returns La Croix', () => {
const beverage = {name: 'La Croix'};
const drink = jest.fn(beverage => beverage.name);
drink(beverage);
expect(drink).toHaveReturnedWith('La Croix');
// drink(beverage) 함수 결과가 'La Croix' 문자열을 반환하는지?
});
test.only("run only", () => {// 이 테스트 함수만 실행됨});
test("not run", () => {// 실행 안됨});
그럼 위와 같이 테스트 결과가 나온다.
* Expect 관련 문서 : https://runebook.dev/ko/docs/jest/expect
jest.fn()의 개념jest.fn()은 Jest에서 제공하는 가짜 함수(mock function) 생성기이다.
이 함수는 실제 로직 없이, “누가 언제 어떤 인자로 호출했는지” 등의 정보를 기록한다.
즉, 테스트 시 실제 코드를 실행하지 않고도
Mock 함수는 실제 함수를 대신해 동작하는 가짜 함수로,
테스트 환경에서 외부 의존성을 제거하고 함수의 호출 여부나 인자 전달만을 검증할 때 사용한다.
Mock 함수는 내부 로직을 수행하지 않고, 테스트에 필요한 호출 기록만 남긴다.
(1번 예시)
// Button.tsx
'use client';
import React from 'react';
type ButtonProps = {
label: string;
onClick?: () => void;
disabled?: boolean;
};
export default function Button({ label, onClick, disabled }: ButtonProps) {
return (
<button onClick={onClick} disabled={disabled}>
{label}
</button>
);
}
// __tests__ > Button.test.tsx
import userEvent from '@testing-library/user-event';
test("버튼을 테스트해보겠어요", () => {
const handleBtn = jest.fn(); //가짜호출
render (<Button label={"테스트 버튼"} onClick={handleBtn} />) // 테스트 컴포넌트를 실제 DOM에 렌더링함
const button = screen.getByRole('button', {name: "테스트 버튼"});
fireEvent.click(button);
expect(handleBtn).toHaveBeenCalled();
expect(handleBtn).toHaveBeenCalledTimes(1);
}
)
test('버튼테스트(userEvent)', async () => {
const user = userEvent.setup();
const handleBtn = jest.fn();
render(<Button label="테스트 버튼" onClick={handleBtn} />);
const button = screen.getByRole('button', { name: '테스트 버튼' });
await user.click(button);
expect(handleBtn).toHaveBeenCalledTimes(1);
});
Button 컴포넌트를 실제 DOM에 렌더링함.(2번 예시)
test("value 바뀌나요?", ()=> {
render(<DefaultInput placeholder={"값입력필요해요"} />);
const input = screen.getByRole('textbox');
fireEvent.change(input, { target: { value: 'value is changed' } });
expect(input).toHaveValue('value is changed');
})
fireEvent.change(input, { target: { value: 'value is changed' } }) input 요소에 “change 이벤트”를 강제로 발생시킨다. 실제 브라우저에서 사용자가 타이핑하는 효과를 시뮬레이션 하는 것{ target: { value: 'value is changed' } }는 이벤트 객체의 형태로 전달된다. 즉, e.target.value = 'value is changed' 와 같은 상황을 흉내내는 것 !!!⚙️ 내부적으로 일어나는 일
<input>의 onChange 핸들러가 호출됨input.value가 새로운 값으로 반영됨expect(input).toHaveValue('value is changed') : 실제 input.value가 바뀌었는지 검증fireEvent는 프로그램적으로 이벤트를 발생시킨다.
click()을 호출한다면 onClick 이라는 Dom Event를 발생시키는 것이다.
userEvent는 전체적인 상호작용을 테스트할 수 있다.
`click()`을 호출한다면 클릭하기까지 발생할 수 있는 다양한 이벤트들을 거치게 된다.
브라우저에서 사용자가 상호작용할 때 지정된 이벤트만 호출하리라는 법은 없다.
`react-testing-library`로 테스트를 한다고 해도 실제로 눈에 보이는 것이 없고 브라우저 환경이 아니기 때문에 개발자가 지정한 이벤트만 모의해서 테스트하는 경우 실제 환경과 차이가 있을 수 있는데 이런점들을 최대한 보완해준 느낌인 것 같다.
> 'userEvent' 예시 :
사용자가 텍스트 상자에 입력할 때 요소에 focus 하는 이벤트가 발생하고 그 다음 키보드 및 입력 이벤트가 발생하고 입력할 때 요소의 선택 및 값이 조작된다.
>
참고문서 : https://velog.io/@pds0309/react-testing-library-fireEvent-vs-userEvent
describe('<Button />', () => {
test.only('라벨이 올바르게 표시된다', () => {
render(<Button label="테스트 버튼" />);
expect(screen.getByRole('button', { name: '테스트 버튼' })).toBeInTheDocument();
}); // only가 붙은 함수만 실행
it('클릭 시 onClick이 호출된다', async () => {
const onClick = jest.fn();
const user = userEvent.setup();
render(<Button label="클릭" onClick={onClick} />);
const button = screen.getByRole('button', { name: '클릭' });
await user.click(button);
expect(onClick).toHaveBeenCalledTimes(1);
});
it('disabled 속성이 true면 클릭되지 않는다', async () => {
const onClick = jest.fn();
const user = userEvent.setup();
render(<Button label="비활성" onClick={onClick} disabled />);
const button = screen.getByRole('button', { name: '비활성' });
await user.click(button);
expect(onClick).not.toHaveBeenCalled();
expect(button).toBeDisabled();
});
});
render(<Button label="테스트 버튼" />)Button 컴포넌트를 실제 DOM에 렌더링함.label="테스트 버튼"이 props로 전달돼서 내부적으로 <button>테스트 버튼</button> 같은 요소가 만들어짐.screen.getByRole('button', { name: '테스트 버튼' })"button"인 요소를 찾음."테스트 버튼"인 걸 찾기 때문에, 실제로 화면에 "테스트 버튼"이라는 레이블을 가진 버튼만 선택됨.screen은 render()로 그린 화면(DOM)을 대표하는 “가상 브라우저 창” 객체render()를 쓰면, 그 컴포넌트가 가짜 브라우저 환경(DOM)에 렌더링된다. 즉, screen은 “렌더링된 DOM 전체를 들여다보는 창”이라고 보면 된다.💡 단순히 텍스트로 찾는 게 아니라,
“사용자가 스크린리더로 읽을 때 ‘테스트 버튼’으로 인식되는 버튼”을 찾는 것 !!
.toBeInTheDocument()"테스트 버튼"이라는 이름을 가진 버튼이 화면에 실제로 존재한다면 ✅ 통과.getByRole과 getByText는 둘 다 DOM 요소를 찾는 Testing Library 쿼리 함수.
getByRole 은 태그명(tag name) 기준이 아닌 WAI-ARIA role 기준으로 찾음
| 메서드 | 찾는 기준 | 예시 |
|---|---|---|
getByRole | 요소의 HTML 역할(role) 기준 | <button>, <textbox>, <link> 등 |
getByText | 화면에 보이는 텍스트 내용 기준 | “로그인”, “회원가입” 등 텍스트 노드 |
<input> 태그의 실제 Role 매핑| HTML | Role 이름 | getByRole() 값 |
|---|---|---|
<input type="text" /> | textbox | 'textbox' ✅ |
<input type="email" /> | textbox | 'textbox' ✅ |
<input type="search" /> | searchbox | 'searchbox' ✅ |
<input type="checkbox" /> | checkbox | 'checkbox' ✅ |
<input type="radio" /> | radio | 'radio' ✅ |
<input type="button" /> | button | 'button' ✅ |
<input type="file" /> | button | 'button' ✅ |
<textarea /> | textbox | 'textbox' ✅ |
추가 참고 문서