WebLog

「フロントエンドのテスト手法まとめ」をなぞる

2022/12/15 19:44

はじめに

この記事は qiita から移行したものです。

勉強として以下の記事をなぞったので記事にしました。ところどころ自己流にアレンジしています。

【入門】フロントエンドのテスト手法まとめ #React - Qiita

【入門】フロントエンドのテスト手法まとめ #React - Qiita

はじめに自分は2021年に新卒でweb系の開発会社にフロントエンジニアとして入社し2022年で2年目になります。実務ではReact×TypeScriptを利用したフロント周りの開発をメインで行な…

環境構築

環境構築が元記事と違います。 以下では、Next.js の公式のセットアップReact-Testing-Library 公式の userEvent のセットアップの記事を参考にしています。

1# 現在のディレクトリに「test-turorial」という名前でnext.jsアプリを作成
2npx create-next-app --ts --use-npm test-tutorial
3
4# いま作ったディレクトリに移動
5cd test-tutorial
6
7# 今回使うライブラリをインストール
8npm i jest jest-environment-jsdom @testing-library/react @testing-library/jest-dom @testing-library/user-event msw axios

さらに、プロジェクトルートに jest.config.js を作成して以下をコピペします。

jest.confing.js
1const nextJest = require('next/jest')
2
3const createJestConfig = nextJest({
4 // テスト環境のnext.config.jsと.envファイルを読み込むために、Next.jsアプリのパスを指定します
5 dir: './',
6})
7
8// jestに渡されるカスタム設定を追加します
9const customJestConfig = {
10 // 各テストが走る前の設定を追加します
11 // setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
12 // TypeScriptでbaseUrlをルートディレクトリに設定している場合、aliasを動作させるためには以下のようにする必要があります。
13 moduleDirectories: ['node_modules', '<rootDir>/'],
14 testEnvironment: 'jest-environment-jsdom',
15}
16
17// createJestConfigは、next/jestが非同期でNext.jsの設定を読み込めるようにするために、このようにエクスポートされます。
18module.exports = createJestConfig(customJestConfig)

package.json に npm script を追加します。 これにより、「npm run test」でテストが実行できます。

package.json
1...
2
3"scripts": {
4 "dev": "next dev",
5 "build": "next build",
6 "start": "next start",
7 "lint": "next lint",
8+ "test": "jest --verbose"
9 },
10
11...

テスト

コンポーネントのレンダリングテスト

pages/index.tsx
1import type { NextPage } from "next";
2
3const Home: NextPage = () => {
4 return <div>Hello World</div>;
5};
6export default Home;
__tests__/index.test.tsx
1import { render, screen } from "@testing-library/react";
2import Home from "../pages";
3import "@testing-library/jest-dom/extend-expect";
4
5describe("レンダリングテスト", () => {
6 it("画面にHello Worldが表示されていること", () => {
7 render(<Home />);
8 expect(screen.getByText("Hello World")).toBeInTheDocument();
9 });
10});

ユーザーイベントのテスト

components/searchForm.tsx
1import React, { useState } from "react";
2
3export const SearchForm = (): JSX.Element => {
4 const [value, setValue] = useState<string>("");
5 const onchange = (e: React.ChangeEvent<HTMLInputElement>) => {
6 setValue(e.target.value);
7 };
8
9 const onClick = () => {
10 // 検索の処理
11 };
12 return (
13 <div>
14 <input type="text" onChange={onchange} value={value} />
15 <button onClick={onClick}>検索</button>
16 </div>
17 );
18};
__tests__/searchForm.test_tsx
1import { render, screen } from "@testing-library/react";
2import { SearchForm } from "../components/searchForm";
3import userEvent from "@testing-library/user-event";
4import "@testing-library/jest-dom/extend-expect";
5
6describe("ユーザーイベントのテスト", () => {
7 it("フォームに値を入力したときにフォームの値が入力した値になっていること", async () => {
8 render(<SearchForm />);
9 const inputForm = screen.getByRole("textbox") as HTMLInputElement;
10 // userEvent.typeは返り値がPromise型なのでawaitをつける
11 await userEvent.type(inputForm, "test");
12 expect(inputForm.value).toBe("test");
13 });
14});

props でのデータ受け取りのテスト

components/cards.tsx
1type User = {
2 id: number;
3 name: string;
4};
5
6export const Cards = ({ userInfos }: { userInfos: User[] }): JSX.Element => {
7 return (
8 <>
9 {userInfos.length === 0 ? (
10 <p>ユーザー情報は0です</p>
11 ) : (
12 <ul>
13 {userInfos.map((userInfo) => {
14 return (
15 <li key={userInfo.id}>
16 id:{userInfo.id} name:{userInfo.name}
17 </li>
18 );
19 })}
20 </ul>
21 )}
22 </>
23 );
24};
__tests__/cards.tsx
1import { render, screen } from "@testing-library/react";
2import { Cards } from "../components/Cards";
3import "@testing-library/jest-dom/extend-expect";
4
5describe("propsでのデータ受け取りのテスト", () => {
6 it("空配列を渡したときに、「ユーザー情報は0です」と表示されること", () => {
7 render(<Cards userInfos={[]} />);
8 expect(screen.getByText("ユーザー情報は0です")).toBeInTheDocument();
9 });
10
11 it("空でない配列を渡したときに正常に表示されること", () => {
12 const dummyUserInfos = [
13 { id: 1, name: "tom" },
14 { id: 2, name: "mary" },
15 { id: 3, name: "bob" },
16 ];
17 render(<Cards userInfos={dummyUserInfos} />);
18
19 const userInfos = screen
20 .getAllByRole("listitem")
21 .map((item) => item.textContent);
22 const dummyItems = dummyUserInfos.map(
23 (item) => `id:${item.id} name:${item.name}`
24 );
25
26 expect(userInfos).toEqual(dummyItems);
27 });
28});
29

useEffect のテスト

pages/blog.tsx
1import axios from "axios";
2import { NextPage } from "next";
3import { useEffect, useState } from "react";
4
5type Post = {
6 userId: number;
7 id: number;
8 title: string;
9 body: string;
10};
11
12const BlogPage: NextPage = () => {
13 const [postData, setPostData] = useState<Post>();
14
15 const getPost = async (): Promise<Post> => {
16 const response = await axios.get(
17 "https://jsonplaceholder.typicode.com/posts/1"
18 );
19
20 return response.data;
21 };
22
23 useEffect(() => {
24 try {
25 const getData = async () => {
26 const result = await getPost();
27 setPostData(result);
28 };
29 getData();
30 } catch (e: unknown) {
31 console.log(e);
32 }
33 }, []);
34
35 return (
36 <div>
37 {!postData ? (
38 <p>ローディング中</p>
39 ) : (
40 <p>
41 記事ID{postData.id}:{postData.title}
42 </p>
43 )}
44 </div>
45 );
46};
47
48export default BlogPage;
__tests__/blog.test.tsx
1import { render, screen } from "@testing-library/react";
2import BlogPage from "../pages/blog";
3import "@testing-library/jest-dom/extend-expect";
4
5describe("useEffectのテスト", () => {
6 it("データ取得完了前には「ローディング中」と表示されていること", () => {
7 render(<BlogPage />);
8 expect(screen.getByText("ローディング中")).toBeInTheDocument();
9 });
10
11 it("データ取得完了後は「記事ID」を含むテキストが表示されていること", async () => {
12 render(<BlogPage />);
13 expect(await screen.findByText(/記事ID/)).toBeInTheDocument();
14 });
15});

API のテスト

pages/user.tsx
1import axios from "axios";
2import { NextPage } from "next";
3import { useState } from "react";
4
5type User = {
6 id: number;
7 name: string;
8 username: string;
9 email: string;
10};
11const UserPage: NextPage = () => {
12 const [user, setUser] = useState<User>();
13 const [error, setError] = useState<string>("");
14
15 const getUser = async () => {
16 try {
17 const response = await axios.get(
18 "https://jsonplaceholder.typicode.com/users/1"
19 );
20 const { id, name, username, email } = response.data;
21 const userInfo = {
22 id,
23 name,
24 username,
25 email,
26 };
27 setUser(userInfo);
28 } catch (e: unknown) {
29 setError("Request failed.");
30 }
31 };
32
33 return (
34 <div>
35 {!user && !error && (
36 <>
37 <p>データはありません</p>
38 <button onClick={getUser}>ユーザー情報を取得</button>
39 </>
40 )}
41 {user && <h3>名前: {user.name}</h3>}
42 {error && <p data-testid="error">{error}</p>}
43 </div>
44 );
45};
46
47export default UserPage;
__tests__/user.test.tsx
1import { render, screen } from "@testing-library/react";
2import userEvent from "@testing-library/user-event";
3import { rest } from "msw";
4import { setupServer } from "msw/node";
5import UserPage from "../pages/user";
6
7const server = setupServer(
8 rest.get("https://jsonplaceholder.typicode.com/users/1", (_req, res, ctx) => {
9 return res(
10 ctx.status(200),
11 ctx.json({
12 id: 1,
13 name: "Leanne Graham dummy",
14 username: "Bret dummy",
15 email: "Sincere@april.biz.dummy",
16 })
17 );
18 })
19);
20
21beforeAll(() => server.listen());
22afterEach(() => {
23 server.resetHandlers();
24});
25afterAll(() => server.close());
26
27describe("APIのテスト", () => {
28 it("データを取得できた際に正常に表示されること", async () => {
29 render(<UserPage />);
30 await userEvent.click(screen.getByRole("button"));
31 expect((await screen.findByRole("heading")).textContent).toEqual(
32 "名前: Leanne Graham dummy"
33 );
34 });
35
36 it("データを取得できなかった際に「Request failed.」と表示されること", async () => {
37 server.use(
38 rest.get(
39 "https://jsonplaceholder.typicode.com/users/1",
40 (_req, res, ctx) => {
41 return res.once(ctx.status(404), ctx.json({ message: "error" }));
42 }
43 )
44 );
45
46 render(<UserPage />);
47 await userEvent.click(screen.getByRole("button"));
48 expect((await screen.findByTestId("error")).textContent).toEqual(
49 "Request failed."
50 );
51 expect(screen.queryByRole("heading")).toBeNull();
52 });
53});
54

最新の投稿