Jest + React-Testing-Libraryのコードまとめ

JestとReact-Testing-Libraryで使うコードの一覧をまとめておきます。
長いので右下の目次から必要なところに飛んでください。

<details><summary>使用技術</summary>

・React(18)
・React-Testing-Library
・Jest

</details>

## ■画面取得まとめ
### ◎画面の状態を確認する

```tsx
//画面全体
screen.debug();

//画面のx部分のみ
screen.debug(x)
```

### ◎タグ要素で指定(タグ名は下のGitHubに一覧あり)

```tsx
//1つしかない場合
screen.getByRole("heading")

//複数ある時
screen.getAllByRole("button"); 
```

<details><summary>タグ名一覧はこちらより</summary>

[
  [ '{"name": "article"}', [ 'article' ] ],
  [ '{"name": "button"}', [ 'button' ] ],
  [ '{"name": "td"}', [ 'cell', 'gridcell' ] ],
  [ '{"name": "input", "attributes": [ {"name": "type", "value": "checkbox"}] }', [ 'checkbox' ] ],
  [ '{"name": "th"}', [ 'columnheader' ] ],
  [ '{"name": "select"}', [ 'combobox', 'listbox' ] ],
  [ '{"name": "menuitem"}', [ 'command', 'menuitem' ] ],
  [ '{"name": "dd"}', [ 'definition' ] ],
  [ '{"name": "figure"}', [ 'figure' ] ],
  [ '{"name": "form"}', [ 'form' ] ],
  [ '{"name": "table"}', [ 'grid', 'table' ] ],
  [ '{"name": "fieldset"}', [ 'group' ] ],
  [ '{"name": "h1"}', [ 'heading' ] ],
  [ '{"name": "h2"}', [ 'heading' ] ],
  [ '{"name": "h3"}', [ 'heading' ] ],
  [ '{"name": "h4"}', [ 'heading' ] ],
  [ '{"name": "h5"}', [ 'heading' ] ],
  [ '{"name": "h6"}', [ 'heading' ] ],
  [ '{"name": "img"}', [ 'img' ] ],
  [ '{"name": "a"}', [ 'link' ] ],
  [ '{"name": "link"}', [ 'link' ] ],
  [ '{"name": "ol"}', [ 'list' ] ],
  [ '{"name": "ul"}', [ 'list' ] ],
  [ '{"name": "li"}', [ 'listitem' ] ],
  [ '{"name": "nav"}', [ 'navigation' ] ],
  [ '{"name": "option"}', [ 'option' ] ],
  [ '{"name": "input", "attributes": [ {"name": "type", "value": "radio"}] }', [ 'radio' ] ],
  [ '{"name": "frame"}', [ 'region' ] ],
  [ '{"name": "rel"}', [ 'roletype' ] ],
  [ '{"name": "tr"}', [ 'row' ] ],
  [ '{"name": "tbody"}', [ 'rowgroup' ] ],
  [ '{"name": "tfoot"}', [ 'rowgroup' ] ],
  [ '{"name": "thead"}', [ 'rowgroup' ] ],
  [ '{"name": "th", "attributes": [ {"name": "scope", "value": "row"}] }', [ 'rowheader' ] ],
  [ '{"name": "input", "attributes": [ {"name": "type", "value": "search"}] }', [ 'searchbox' ] ],
  [ '{"name": "hr"}', [ 'separator' ] ],
  [ '{"name": "dt"}', [ 'term' ] ],
  [ '{"name": "dfn"}', [ 'term' ] ],
  [ '{"name": "textarea"}', [ 'textbox' ] ],
  [ '{"name": "input", "attributes": [ {"name": "type", "value": "text"}] }', [ 'textbox' ] ],
]
</details>

https://github.com/A11yance/aria-query

### ◎文字列で指定

```tsx
//必ず存在する場合
screen.getByText("Udemy");

//存在するかどうかわからない場合の取得方法(nullを確認したい際など)
screen.queryByText("Udeeeemy");
```

### ◎テストIDを付与して取得

```html:HTML
<span data-testid="copyright">@React</span>
```

```tsx:JavaScript
screen.getByTestId("copyright");
```

### ◎Inputタグ

```tsx
//placeholer名で取得
screen.getByPlaceholderText("Enter");
```

## ■expextのメソッドまとめ

```tsx
//xが存在している
expect(x).toBeTruthy();

//xが存在していない、値がない
expect(x).toBeNull();

//x = yか否か(表示というより値の判断)
expect(x).toBe(y);
expect(x).toEqual(y);

//yがxに含まれているか
expect(x).toBeInTheDocument(y);
expect(x).toHaveTextContent(y);

//そのHTMLに属性(今回はdisabled)が存在するか否か
expect(x).toHaveAttribute("disabled");
```

## ■after, beforeメソッド
```tsx
//そのテストファイルが始まる際、1度だけ発動
beforeAll(() => {});

//itが始まる度に発動
beforeEach(() => {});

//itが終わる度に発動
afterEach(() => {});

//そのテストファイルが終わる際、1度だけ発動
afterAll(() => {});
```

### ◎再レンダリングの前に一旦前のレンダリング情報を削除

```tsx
import { render, screen, cleanup } from "@testing-library/react";

afterEach(() => cleanup());
```

### ◎疑似サーバ(API)を立てる際に必要

```tsx
//このテストファイルを発動する前に、1度だけ発動する(今回は疑似サーバを立てる)
beforeAll(() => server.listen());

//itを使う度に発動:レンダリングデータの削除
afterEach(() => {
  cleanup();
});

//このテストファイルが終わった後に発動:サーバを閉じる
afterAll(() => {
  server.close();
});
```
## ■APIのモック
### ◎APIで取得するデータをダミーで作成する

#### エラー時の確認
・async, awaitをつけないとエラー時のテストが出来る
・その文字が無い場合のテストの際は「queryByText」で情報を取得すること
・取得した情報がnullである事を確認する(toBeNull)

```tsx
it("データが入っていない場合、表示されないこと", () => {
    render(<SampleComponent />);

    //Iamを取得(/で囲むと部分一致検索) → nullならOK
    const errorScreen = screen.queryByText(/I am/);
    expect(errorScreen).toBeNull();
  });
```

#### 取得出来ている場合のテスト
・async, awaitをつけること
・「findByText」で取得
・toBeInTheDocumentで画面情報に「I am」が含まれているかを確認

```tsx
it("データがきちんと取得出来ること", async () => {
    render(<SampleComponent />);

    //Iamを取得(/で囲むと部分一致検索) → 取得出来ればOK
    const userDataScreen = await screen.findByText(/I am/);
    expect(userDataScreen).toBeInTheDocument();
  });
```

### ◎APIをダミーで作成する場合
#### 事前準備

①mswをインストール

```plaintext:コマンドプロンプト or ターミナル
npm i -D msw@0.39.2
```


②インポート文準備

```tsx
import React from "react";
import { render, screen, cleanup, act } from "@testing-library/react";

//ユーザのイベントをテストするため
import userEvent from "@testing-library/user-event";

//ダミーAPI作成用
import { rest } from "msw";
import { setupServer } from "msw/node";

//components
import SampleComponent from "./SampleComponent";
```

③before, after文記述

```tsx
//このテストファイルを発動する前に、1度だけ発動する(今回は疑似サーバを立てる)
beforeAll(() => server.listen());

//itを使う度に発動:レンダリングデータの削除
afterEach(() => {
  cleanup();
});

//このテストファイルが終わった後に発動:サーバを閉じる
afterAll(() => {
  server.close();
});
```

#### 疑似APIの作成

```tsx
/**
 * 指定したURLにアクセスすると、実際のURLでなく下記ダミーAPIにアクセスした事にする.
 */
const server = setupServer(
  rest.get("http://dummyurl", (req, res, ctx) => {
    //ステータスと返すデータの指定
    return res(ctx.status(200), ctx.json({ username: "dummyName" }));
  })
);

```

#### テストコード作成

テストするメソッドの中でsetState使っているとエラーになる
→setStateが発動するユーザのアクションをactで囲む必要がある

```tsx
describe("Sampleコンポーネントテスト", () => {
  it("APIを叩いて問題なく表示されること", async () => {
    render(<SampleComponent />);

    //ボタンをクリックしてAPIからデータを取得する
    const buttonScreen = screen.getByRole("button");
    //   userEvent.click(buttonScreen);　→これだとactで囲んでないのでダメ！
    act(() => {
      buttonScreen.dispatchEvent(new MouseEvent("click", { bubbles: true }));
    });

    //ユーザ情報が表示されているか否か
    const userNameScreen = await screen.findByRole("heading");
    screen.debug(userNameScreen);
    expect(userNameScreen).toHaveTextContent("dummyName");
  });
});
```

## ■Hooksのテスト
### ◎必要なモジュールのインポート

```plaintext:コマンドプロンプト or ターミナル
npm i @testing-library/react-hooks
npm i react-test-renderer
```

### ◎インポート文等を書く

```tsx
import { render, screen, cleanup } from "@testing-library/react";

//hooksを使うのに必要！
import { act, renderHook } from "@testing-library/react-hooks";

//hooks
import { useSample } from "./useSample";
```

```tsx
afterEach(() => cleanup());
```

### ◎Hooksの呼び出し
renderHooksを使うとHooksの呼び出しが出来る

```tsx
const { result } = renderHook(() => useSample());
//resultにHooksの情報が入る
//result.current.変数名やメソッド名 で現在の変数等の状態を取得出来る
```

```tsx
describe("Hooksテスト", () => {
  it("初期値3が正しく表示されるか",  () => {
    //Hooksの利用
    const { result } = renderHook(() => useCounter(3));

    const initialResult = result.current.count;
    expect(initialResult).toBe(3);
  });
});
```

APIの時と同じように、メソッド内でsetStateが行われている際には
メソッドの発動タイミングをactで囲む必要がある

```tsx
it("初期値3から1を引くメソッドのテスト", () => {
    //Hooksの利用
    const { result } = renderHook(() => useSample(3));

    //メソッドの呼び出し
    act(() => {
      result.current.decrement();
    });

    //Hooksから変数「count」を呼び出し、値が2になっているかチェック
    const decrementResult = result.current.count;
    expect(decrementResult).toBe(2);
  });
```


## ■act
テスト対象のメソッド内で「setState」使っている場合は
そのメソッドが発動するタイミングをactで囲む必要がある

```tsx
import { act } from "react-dom/test-utils";

...

const buttonScreen = screen.getByRole("button");

//NG!
userEvent.click(buttonScreen);

//OK!
act(() => {
  buttonScreen.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
```

### ◎Hooksのact

```tsx
import { act } from "@testing-library/react-hooks";

//OK!:直接メソッドを呼ぶ
act(() => {
  sampleMethod();
});
```

## ■まとめ
今回はこれ1ページ見ればとりあえずある程度のJestは網羅できるだろう、となるようにまとめてみました。フロントのテストってそもそも何をすればいいの？という方は下記も併せてご覧頂けると幸いです。

https://qiita.com/koda-momo/items/7373340dfc40030a79b7

ここまでご覧いただきありがとうございました！

## ■おまけ
### ◎テストを試したGitHub

https://github.com/koda-momo/rtl_lesson

### ◎参考URL

https://www.udemy.com/course/reacthooksreduxtoolkit/

https://jestjs.io/ja/

https://testing-library.com/docs/react-testing-library/intro/

https://reactjs.org/docs/test-utils.html