react-testing-libraryについて
Prefix
getBy, getAllBy
要素がそこにあることを証明する場合
test("favor getBy when proving an element exist", () => {
render(<Component />);
const element = screen.getByRole("list");
expect(element).toBeInTheDocument();
}
)
queryBy, queryAllBy
要素がそこにないことを証明する場合
test("favor getBy when proving an element does not exist", () => {
render(<Component />);
const element = screen.queryByRole("textbox");
expect(element).not.toBeInTheDocument();
}
)
無いことを確認するときにqueryBy
を使う理由は、DOMにない要素をgetBy
の引数に入れた時点でエラーを投げてしまうから。
findBy, findAllBy
非同期を扱う場合
test("favor findBy or findAllBy when data fetching", async () => {
render(<Component />);
const elements = await screen.findAllByRole("listitem");
expect(elements).toHaveLength(3);
}
)
Suffix
suffix | 検索基準 | どれ使う? |
---|---|---|
ByRole | 明示的・暗黙的なARIA roleを検索 | 基本的にはこれ |
ByLabel | ラベルテキストを検索 | |
ByPlaceholderText | プレースホルダーを検索する | |
ByText | テキストを検索する | 次候補はこれ |
ByDisplayValue | 現在の値から検索 | |
ByAltText | alt属性を検索 | |
ByTitle | title属性を検索 | |
ByTestId | data-testid属性を検索 | これはお勧めできない最後の手段 |
Custom Matcher
基本のMatcherは以下を参照。
以下のコンポーネントをテストしたい。
export const FormData = () => {
return (
<div>
<button>Do something</button>
<form aria-label="form">
<button>Save</button>
<button>Cancel</button>
</form>
</div>
);
}
within
でラップすればいいが、毎回これを書くのは大変なので...
test("the form display two buttons", () => {
render(<FormData />);
const form = screen.getByRole("form");
const buttons = within(form).getAllByRole("button");
expect(buttons).toHaveLength(2)
})
カスタムマッチャーを使ってテストを書く!
テスト全体で使えるようにjest.setup.ts
にカスタムマッチャーを追加する
import "@testing-library/jest-dom/extend-expect";
import { expect } from "@jest/globals";
import { within } from "@testing-library/react";
import { server } from "./mocks/server.js";
import type { ByRoleMatcher } from "@testing-library/react";
import type { MatcherFunction } from "expect";
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
const toContainRole: MatcherFunction<[role: ByRoleMatcher, quantity: number]> = (
actual,
role,
quantity
) => {
const element = within(actual as HTMLElement).queryAllByRole(role);
if (element.length === quantity) {
return { message: () => "true", pass: true };
}
return {
message: () =>
`Expected to find ${quantity} ${role} elements. Found ${element.length} instead.`,
pass: false,
};
};
expect.extend({ toContainRole });
const customJestConfig = {
...
setupFilesAfterEnv: ["./jest.setup.ts"],
...
};
型を追加
/* eslint-disable @typescript-eslint/no-empty-interface */
import { ByRoleMatcher } from "@testing-library/react";
interface CustomMatchers<R = unknown> {
toContainRole(role: ByRoleMatcher, quantity: number): R;
}
declare global {
namespace jest {
interface Expect extends CustomMatchers {}
interface Matchers<R> extends CustomMatchers<R> {}
interface InverseAsymmetricMatchers extends CustomMatchers {}
}
}
export {};
1ラインでかける!
test("the form display two buttons", () => {
render(<FormData />);
const form = screen.getByRole("form");
expect(form).toContainRole("button", 2);
})
【エラー遭遇】form要素が見つからない
Unable to find an accessible element with the role "form"
のエラーが出たら、form要素にaria-label="form"
を追加するとエラーが消える。
非同期のテストについて
useEffectなどでデータを取得する場合にact(() => { ...
のような警告が出る場合がある。
これは内部で非同期処理が行われているため。解決法はいくつかあるが1がBestで3がWorst。
findBy
findAllBy
1 ほどんどのケースはfindBy
findAllBy
で解決させる。これらを用いることで、コンポーネントで行われているデータ取得が終了したことを検出することができる。
const renderComponent = () => {
const repository = {
full_name: "facebook/react",
language: "Javascript",
description:
"A declarative, efficient, and flexible JavaScript library for building user interfaces.",
owner: {
login: "facebook",
},
name: "react",
html_url: "https://github.com/facebook/react",
};
render(
<MemoryRouter>
<RepositoriesListItem repository={repository} />
</MemoryRouter>
);
return { repository };
};
test("shows a link to the github homepage for this repository", async () => {
const { repository } = renderComponent();
// actエラー回避
await screen.findByRole("img", { name: /javascript/i });
const link = screen.getByRole("link", { name: /github repository/i });
expect(link).toHaveAttribute("href", repository.html_url);
});
test("shows a fileIcon with the appropriate icon", async () => {
renderComponent();
const icon = await screen.findByRole("img", { name: /javascript/i });
expect(icon).toHaveClass("js-icon");
});
test("shows a link to the code editor page", async () => {
const { repository } = renderComponent();
// actエラー回避
await screen.findByRole("img", { name: /javascript/i });
const link = await screen.findByRole("link", {
name: new RegExp(repository.owner.login),
});
expect(link).toHaveAttribute("href", `/repositories/${repository.full_name}`);
});
<a href={repository.html_url} aria-label="github repository">
<MarkGithubIcon />
</a>
2 Module Mocks
テストしたい箇所に直接関係ないところでエラーが発生している場合、その箇所をモックを用いて解決する。jest.mock
は第一引数で指定したコンポーネントを実際にはインポートせず、return内の要素を返すようにしてくれる。
...
jest.mock("../tree/FileIcon.js", () => {
// FileIcon.jsのコンテンツ
return () => {
return "File Icon Component";
};
});
3 act
まじでこの方法は使いたくない。**最後の切り札。**このテクニックは使わないことをおすすめする。
詳細はUdemyで。
fetchが関連するテスト(msw)
mswを用いてAPIをインターセプトする。実際のプロジェクトではたくさんのmockルートを定義しなければいけないので以下のようなcreateServer
関数でセットアップを効率的にする。
import { rest } from "msw";
import { setupServer } from "msw/node";
export const createServer = (handlerConfig) => {
const handlers = handlerConfig.map((config) => {
return rest[config.method || "get"](config.path, (req, res, ctx) => {
return res(ctx.json(config.res(req, res, ctx)));
});
});
const server = setupServer(...handlers);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
};
テストファイルで読み込むとクリーンな記述になる!
import { createServer } from "../test/server";
createServer([
{
path: "/api/repositories",
method: "get",
res: (req, res, ctx) => {
const language = req.url.searchParams.get("q").split("language:")[1];
return {
items: [
{ id: 1, full_name: `${language}_one` },
{ id: 2, full_name: `${language}_two` },
],
};
},
},
]);
test("something to expect", () => {
...
})
describe
のスコープでラップするとより良い
createServerはcreateServer関数にテストの初期化をまとめたのは、単純にキレイになるだけじゃない。testファイル内で使用するときにdescribe
のスコープでラップするとdescribeごとにサーバーを初期化してくれる。
そのため同じファイル内でdescripeスコープごとに異なるレスポンスを返すことができる。
const renderComponent = () => {
return (
<MemoryRouter>
<AuthButtons />
</MemoryRouter>
);
};
describe("when user is not signed in", () => {
createServer([
{
path: "/api/user",
method: "get",
res: (req, res, ctx) => {
return { user: null };
},
},
]);
test("when user is not signed in, signin and signup are visible", async () => {
renderComponent();
});
test("when user is not signed in, signout is not visible", async () => {
renderComponent();
});
});
describe("when user is signed in", () => {
createServer([
{
path: "/api/user",
method: "get",
res: (req, res, ctx) => {
return { user: { id: 3, email: "test@gmail.com" } };
},
},
]);
test("when user is not signed in, signin and signup are not visible", async () => {
renderComponent();
});
test("when user is not signed in, signout is visible", async () => {
renderComponent();
});
});
外部ライブラリー使用時のテスト
外部ライブラリ(例: SWRなど)が関連するテストは間違いなく大半の時間を費やすことになる 😫
もしswrでのuser取得をモックしてテストが失敗した場合、ライブラリーがjest用にうまく動いてはくれないことを認識する必要がある。 以下、解決方法の戦略を順に記載する。
describe.only
test.only
1. スコープを絞ることで、失敗箇所を限定させる。
2. debuggerの設定
以下をpackage.json
のscripts
へ追加
このコマンドは、テストを特殊なモードで実行し、テストランナーに「すべてのテストをすぐに実行する必要はない」と伝えます。その代わり、最初に見つけたdebugger;
で一時停止するようにします。
"scripts": {
...
"test:debug": "react-scripts --inspect-brk test --runInBand --no-cache",
},
--inspect-brk
指定ファイルの1行目にブレークポイントを設定した状態にできます。
--runInBand
複数のテストファイルが1つずつ順番に処理されます。
--runInBand
複数のテストファイルが1つずつ順番に処理されます。
--no-cache
キャッシュの利用を無効にする。
調べたいコードの直前にdebugger;
を追加
function AuthButtons() {
const { user, isLoading } = useUser();
// これ!!!!!
debugger;
if (isLoading) {
return null;
} else if (user) {
return (
...
調べたいテストにtest.only
をつけ、直後にdebugger;
を追加
test.only("signin and signup are visible", async () => {
debugger;
await renderComponent();
const signInButton = screen.getByRole("link", { name: /sign in/i });
3. ブラウザの開発者ツールで調査
ターミナルで*about:inspect
を入力
以下の画面に遷移するのでinspect
をクリック
開発者ツールが新たに開き、テストの一番最初の行でポーズしている。先程追加したdebugger;
まで進みたいので、右上の青い再生ボタンをクリック。
すると追加したdegugger;
を順番に推移する。その時の状態のuser
を確認。
初期状態user
の中身はundefined
-> ok。
コンポーネントレンダリング直後null
-> まだfetchしてないからok。
useSWRでフェッチ後null
-> !!!!!??? これが問題だ!!
コンポーネントもmswもtestも問題ない... 問題は、、、キャッシュや!!
useSWRのキャッシュをリセットする方法をググると出てくる。
const renderComponent = async () => {
render(
<SWRConfig value={{ provider: () => new Map() }}>
<MemoryRouter>
<AuthButtons />
</MemoryRouter>
</SWRConfig>
);
await screen.findAllByRole("link");
};
解決!!!