[ 살펴보기 ] React Testing Library - Setup & Queries
![[ 살펴보기 ] React Testing Library - Setup & Queries](https://cdn.hashnode.com/res/hashnode/image/upload/v1729593328863/87c3496e-0575-40aa-9b6c-c82093144664.jpeg)
DOM Testing Library는 site를 구성하는 DOM nodes을 테스트하기 위한 여러 utility를 제공한다. React Testing Library는 DOM Test Library를 기반으로 React framework project testing에 필요한 추가 기능을 제공한다.
해당 포스트는 NextJS project를 기준으로 React Testing library를 사용하는 법을 살펴본다. React Testing Library나 DOM Testing Library는 test runner가 아니기 때문에 보통 test runner 역할을 하는 다른 testing library와 함께 사용하는데 해당 포스트에선 jest를 사용한다.
우선 NextJS에서 jest를 사용하기 위해 필요한 package를 설치하고 configuration을 설정한다.
npm install -D jest jest-environment-jsdom @testing-library/react @testing-library/dom @testing-library/jest-dom
jest : test case를 작성, 실행하고 결과를 알려주는 testing library.
jest-environment-jsdom : jest를 통해 test case를 실행하면 test code는 default로 node환경에서 실행된다. Component를 테스트 하기 위해선 browser가 제공하는 DOM 환경이 구성되어 있어야 하는데 jsdom이 이와 비슷한 환경을 제공해준다. jest-environment-jsdom은 jest configuration중 test 환경을 jsdom으로 설정하기 위해 필요하다.
@testing-library/react: test환경에서 component를 render하고 특정 element를 선택할 수 있게 해준다.@testing-library/domlibrary를 기반으로 하며 React project에 필요한 testing을 위해 추가 api를 제공한다.@testing-library/dom:@testing-library/reactpackage의 peerDependency이므로 함께 설치해준다.@testing-library/jest-dom: jest에서 사용할 수 있는 추가 matcher를 제공한다. toBeInTheDocument이나 toHaveTextContent와 같은 jest에는 없는 matcher를 제공해 jest의 기능을 확장한다.
Project root 경로에 jest.setup.ts 파일을 생성하고 다음과 같이 @testing-library/jest-dom pacakge를 import 해준다.
import "@testing-library/jest-dom";
위와 같이 @testing-library/jest-dom pacakge를 import하는 파일을 따로 생성해놓는 이유는 jest 설정 파일 중 setupFilesAfterEnv 옵션에 위의 파일을 설정하기 위함이다. 위에서 @testing-library/jest-dom package는 jest가 기본적으로 지원하지 않는 추가 matcher를 지원하기 위해 사용된다고 소개했다.
즉, 해당 package가 지원하는 matcher를 사용하기 위해선 test 파일 마다 @testing-library/jest-dom package를 import 해야 하는데 jest 설정 파일 중 setupFilesAfterEnv 옵션에 jest.setup.ts 파일을 설정하면 test 파일 마다 일일이 @testing-library/jest-dom package를 import 하지 않더라도 test code 실행 전 단계에서 jest.setup.ts 파일을 실행하여 @testing-library/jest-dom package를 import하기 된다.
이제 jest 설정을 위해 project root 경로에 jest.config.ts 파일을 생성하고 필요한 설정을 해준다. 아래는 간단한 테스트를 위한 설정이며 필요에 따라 추가 설정을 추가해준다. NextJS project의 Jest 설정은 아래의 예제와 같이 next/jest를 통해 jest config를 wrapping 하여 export한다.
import type { Config } from "jest";
import nextJest from "next/jest.js";
const createJestConfig = nextJest({
dir: "./",
});
const config: Config = {
moduleNameMapper: {
"^@/(.*)$": "<rootDir>/src/$1",
},
moduleFileExtensions: ["js", "ts", "tsx"],
testEnvironment: "jsdom",
setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"],
};
export default createJestConfig(config);
위와 같이 next/jest를 통해 jest config를 export 해주면 NextJS project에서 작업할 때 필요한 다음 설정을 자동으로 추가해준다.
NextJS compiler를 사용하도록 transform option을 설정한다.
Stylesheets, image, next/font에 대한 auto mocking.
.env에 설정된 환경변수를 process.env에 load한다.
Test를 위한 test file resolving & transformatioin 과정에서 node_modules를 제외한다.
Test를 위한 test file resolving 과정에서 .next build 파일을 제외한다.
필자는 tsconfig.json 파일에 module alias를 다음과 같이 설정하여 사용하고 있으므로 test file에서 module alias를 통해 import를 해도 path resolution에 문제가 없도록 jest.config.ts 파일에 moduleNameMapper option을 통해 해당 사항을 설정하고 있다.
// tsconfig.json
...
"compilerOptions": {
...
"paths": {
"@/*": ["./src/*"]
}
}
...
// jest.config.ts
...
const config: Config = {
moduleNameMapper: {
"^@/(.*)$": "<rootDir>/src/$1",
},
...
};
...
그리고 jest 실행을 위해 package.json에 아래와 같이 새로운 script를 추가해준다.
{
...
"scripts": {
...
"test": "jest"
},
...
}
jest.config.ts file이 typescript file이므로 jest 실행 시 ts-node 설치가 필요하다고 에러가 발생할 수 있다. 만약 에러가 발생하면 ts-node를 dev dependency로 추가해주자.
이제 다음과 같은 간단한 Button component가 있고 해당 button component에 대한 test case를 작성한다고 가정해보자.
import React from "react";
type Props = {
label: string;
onButtonClick?: () => void;
};
const Button = ({ label, onButtonClick }: Props) => {
return (
<button className="p-2 border-2 border-orange-300" onClick={onButtonClick}>
{label}
</button>
);
};
export default Button;
그리고 Button.test.tsx 파일을 생성해 아래와 같이 test case를 작성해준다.
import { render, screen } from "@testing-library/react";
test("Render Button", () => {
render(<Button label="test button" />);
const testEl = screen.getByText("test button");
expect(testEl).toBeInTheDocument();
});
위의 예제에서 사용된 api를 살펴보자.
test :
jest에서 제공하는 test case를 작성하기 위한 function이며 첫 번째 parameter로 test name과 두 번째 parameter로 test할 내용이 담길 function 그리고 세 번째 parameter로 test에 대한 timeout 값을 milliseconds로 받는다. default는 5 seconds로 설정되어 있다render :
@testing-library/react에서 제공하는 function이며 test case에서 특정 component dom에 접근할 수 있도록 component를 render한다.screen : 다양한 query method를 제공하며 query method를 통해 특정 dom node를 찾기 위해 사용된다. 위의 예제에서는 “test button”이라는 text를 가지고 있는 node를 찾는다. 위의 예제에선
@testing-library/react에서 제공하고 있지만 내부적으로는@testing-library/dom의 screen object를 re-export하고 있다.expect : 테스트 대상이 예상한 결과와 맞는지 체크할 때 사용하는
jestfunction이다. 위의 예제와 같이 테스트 대상을 argument로 전달하고 위에 특정 matcher를 붙여 테스트 대상이 예상한 결과와 동일한지 점검한다. 위의 예제에선 toBeInTheDocument matcher를 사용한다.toBeInTheDocument :
jest-dom이 제공하는 추가 macher다. 테스트 대상이 document.body에 존재하는지 점검한다.
위의 테스트에선”test button”라는 text를 가진 Button component가 정상적으로 render되었는지 테스트한다. 이제 npm run test를 통해 test를 실행해보자.
PASS __test__/Button.test.tsx
√ Render Button (22 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.507 s, estimated 2 s
작성한 test case가 예상했던 결과에 부합하면 위의 결과와 같이 성공 메시지를 확인할 수 있다. 이번엔 테스트가 실패 하도록 위의 예제 코드를 다음과 같이 변경해보자.
import { render, screen } from "@testing-library/react";
test("Render Button", () => {
render(<Button label="test button2" />);
const testEl = screen.getByText("test button");
expect(testEl).toBeInTheDocument();
});
이번에는 component를 render할 때 label prop으로 test button2라는 값을 전달했기에 render되는 component 역시 test button2라는 text를 가지고 있을 것이다. 그리고 getByText query를 통해 “test button”이라는 text를 가진 dom node를 찾고 있고 해당 node는 render한 component에 존재하지 않으므로 expect(myEl).toBeInTheDocument(); 구문에서 에러가 발생한다.
FAIL __test__/Button.test.tsx
× Render Button (25 ms)
● Render Button
... Unable to find an element with the text: test button.
...
Queries
위의 예제에서는 특정 node를 찾기 위해 getByText query를 사용했지만 다른 방법을 통해서도 특정 node를 찾을 수 있다. 우선 @testing-library/dom package에서 사용할 수 있는 query 타입은 세 가지로 나뉜다.
getBy 타입 query : 조건과 일치하는 node를 발견하면 해당 node를 return하고 발견하지 못하면 error를 throw한다. 조건과 일치하는 node가 하나 이상일 때도 error를 throw한다.
queryBy 타입 query : 조건과 일치하는 node를 발견하면 해당 node를 return하고 발견하지 못하면 null을 반환한다. 조건과 일치하는 node가 하나 이상일 때는 error를 throw한다.
findBy 타입 query : 서치의 결과를 promise로 반환한다. 조건과 일치하는 node를 발견하면 해당 node를 fulfilled 상태의 promise로 반환하고, 조건과 일치하는 node를 발견하지 못하거나 일치하는 node가 하나 이상일 때 rejected 상태의 promise를 반환한다.
getAllBy 타입 query : 조건과 일치하는 node를 모두 array 형식으로 반환한다. 조건과 일치하는 node를 발견하지 못하면 error를 throw한다.
queryAllBy 타입 query : 조건과 일치하는 node를 모두 array 형식으로 반환한다. 조건과 일치하는 node를 발견하지 못하면 빈 array를 반환한다.
findAllBy 타입 query : 서치의 결과를 promise로 반환한다. 조건과 일치하는 node를 발견하면 모든 node를 array로 fulfilled 상태의 promise를 반환하고 조건과 일치하는 node를 발견하지 못하면 rejected 상태의 promise를 반환한다.
getByRole
Browser는 DOM tree를 생성한 뒤 screen reader와 같은 보조 도구를 통해서도 페이지 구조를 이해할 수 있도록 DOM tree를 기반으로 accessibility tree를 생성한다. 그리고 accessibility를 구성하는 각 accessibility object는 name, description, role, state properties로 구성되는데 이 중 role property를 이용해 특정 dom node를 찾는다.
( Reference 1 - Accessibility tree ), ( Reference 2 - Html elements role )
각 html element에 default로 적용되는 role이 있으며 button element의 role은 button이다. button element의 role을 임의로 변경하지 않은 상황이라면 button role을 기준으로 Button component를 찾을 수 있다.
import { render, screen } from "@testing-library/react";
test("Render Button", () => {
render(<Button label="test button2" />);
const testEl = screen.getByRole("button");
expect(testEl).toBeInTheDocument();
});
위의 테스트 코드를 실행해보면 test case가 성공적으로 통과하는 것을 확인할 수 있다.
PASS __test__/Button.test.tsx
√ Render Button (96 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 2.358 s
이제 button element의 role을 다른 role로 변경하고 다시 테스트 해보자.
import React from "react";
type Props = {
label: string;
onButtonClick?: () => void;
};
const Button = ({ label, onButtonClick }: Props) => {
return (
<button
role="article"
className="p-2 border-2 border-orange-300"
onClick={onButtonClick}
>
{label}
</button>
);
};
export default Button;
위의 예제처럼 button의 role을 article로 임의로 변경하고 다시 테스트를 실행하면 button role을 가진 node를 찾을 수 없기에 테스트가 실패하는 것을 확인할 수 있다.
FAIL __test__/Button.test.tsx (13.499 s)
× Render Button (73 ms)
● Render Button
... Unable to find an accessible element with the role "button"
getByLabelText
getByLabelText query를 통해 label element과 연동된 input element를 찾을 수 있다. 테스트를 위해 아래와 같은 TextInput component를 추가했다고 가정하자.
import React from "react";
const TextInput = () => {
return (
<div>
<label htmlFor="email">Email Input</label>
<input id="email" />
</div>
);
};
export default TextInput;
그리고 다음 test code는 “Email Input”이라는 text를 가진 label element와 연동된 input element를 찾고 정상적으로 render되었는지 검사한다.
import { render, screen } from "@testing-library/react";
import TextInput from "@/components/inputs/TextInput";
test("Render Input", () => {
render(<TextInput />);
const testEl = screen.getByLabelText("Email Input");
expect(testEl).toBeInTheDocument();
});
주의할 점은 getByLabelText query이 return하는 node는 label node와 연동된 input node지 label node가 아니다.
getByPlaceholderText
Placeholder property값을 기준으로 node를 찾을 수 있다. 테스트를 위해 TextInput component의 input element에 placeholder property를 추가해보자.
import React from "react";
const TextInput = () => {
return (
<div>
<label htmlFor="email">Email Input</label>
<input id="email" placeholder="user email" />
</div>
);
};
export default TextInput;
그리고 placeholder를 기준으로 node를 찾기위해 다음과 같이 test code를 작성할 수 있다.
import { render, screen } from "@testing-library/react";
import TextInput from "@/components/inputs/TextInput";
test("Render Input", () => {
render(<TextInput />);
const testEl = screen.getByLabelText("Email Input");
expect(testEl).toBeInTheDocument();
});
getByText
특정 text node를 가진 element를 찾을 수 있다. 테스트를 위해 다음과 같이 container component를 추가해보자.
import React from "react";
const Container = () => {
return <div>test value</div>;
};
export default Container;
위의 예제에서 div element는 test value라는 text node를 가지고 있다. 이를 기준으로 element를 찾기 위해선 다음과 같이 test code를 작성할 수 있다.
import { render, screen } from "@testing-library/react";
import Container from "@/components/container/Container";
test("Render Container", () => {
render(<Container />);
const testEl = screen.getByText("test value");
expect(testEl).toBeInTheDocument();
});
getByDisplayValue
Input, select과 같은 element의 value property 값을 기준으로 element를 찾을 수 있다. 테스트를 위해 TextInput component의 input element에 value property를 추가해보자.
import React from "react";
const TextInput = () => {
const handleInputChange = () => { ... };
return (
<div>
<label htmlFor="email">Email Input</label>
<input
id="email"
value="abc12345@gmail.com"
onChange={handleInputChange}
/>
</div>
);
};
export default TextInput;
위의 element를 찾기 위해 test code를 다음과 같이 작성할 수 있다.
import { render, screen } from "@testing-library/react";
import TextInput from "@/components/inputs/TextInput";
test("Render Input", () => {
render(<TextInput />);
const testEl = screen.getByDisplayValue("abc12345@gmail.com");
expect(testEl).toBeInTheDocument();
});
getByAltText
image와 같이 alt property를 사용할 수 있는 element의 alt value를 기준으로 element를 찾을 수 있다. 테스트를 위해 다음과 같은image component를 추가해보자.
import React from "react";
const Image = () => {
return (
<div>
<img src="profile.jpg" alt="my profile" />
</div>
);
};
export default Image;
위의 element를 찾기 위해 test code를 다음과 같이 작성할 수 있다.
import { render, screen } from "@testing-library/react";
import Image from "@/components/image/Image";
test("Render Image", () => {
render(<Image />);
const testEl = screen.getByAltText("my profile");
expect(testEl).toBeInTheDocument();
});
getByTitle
html element에 설정된 title property 값을 기준으로 element를 찾을 수 있다.
import React from "react";
const Container = () => {
return <div title="test container">test value1</div>;
};
export default Container;
위의 element를 찾기 위해 test code를 아래와 같이 작성할 수 있다.
import { render, screen } from "@testing-library/react";
import Container from "@/components/container/Container";
test("Render Container", () => {
render(<Container/> );
const testEl = screen.getByAltText("my profile");
expect(testEl).toBeInTheDocument();
});
getByTestId
element에 추가된 data-testid value를 기준으로 특정 element를 찾을 수 있다.
import React from "react";
const Container = () => {
return <div data-testid="test id">test value1</div>;
};
export default Container;
위의 element를 찾기 위해 test code를 아래와 같이 작성할 수 있다.
import { render, screen } from "@testing-library/react";
import Container from "@/components/container/Container";
test("Render Container", () => {
render(<Container />);
const testEl = screen.getByTestId("test id");
expect(testEl).toBeInTheDocument();
});
![[ 살펴보기 ] RDB - Relationships](https://cdn.hashnode.com/res/hashnode/image/upload/v1739711556668/48dc9e84-a621-42aa-9c9f-5fc5c436f0ec.jpeg)
![[ 살펴보기 ] MySQL - Data types](https://cdn.hashnode.com/res/hashnode/image/upload/v1739593589113/530f8704-4d27-42c9-a451-bb5c63150b99.jpeg)
![[ 살펴보기 ] TypeORM - Transactions, Migration](https://cdn.hashnode.com/res/hashnode/image/upload/v1739106042581/980b8133-61d4-406a-a026-65be9c28eace.jpeg)
![[ 살펴보기 ] TypeORM - Relations](https://cdn.hashnode.com/res/hashnode/image/upload/v1738666874402/b688bd0b-b6bb-4f43-87d8-c1b46b59f1b7.jpeg)
![[ 살펴보기 ] TypeORM - Basics](https://cdn.hashnode.com/res/hashnode/image/upload/v1738666803591/bef5df17-7dc7-4123-ae55-004d5042df39.jpeg)