フロントエンドのテスト入門

Vitestを使ったフロントエンドのテストを試してみます


## 準備
### Reactのインストール

```terminal:terminal
 //インストール開始
 npm create vite@latest
 
 //プロジェクト名入力
 Select a framework: » React
 Select a variant: » TypeScript + SWC
 
 //移動
 cd プロジェクト名
 
 //パッケージインストール
 npm i
 
 //起動
 npm run dev
 ```
 表示に従って http://localhost:5173/ を開きます
Vite + Reactの画面が立ち上がればokです


### ビルドする場合
```:terminal
 //ビルド(distディレクトリに静的に出力)
 npm run build
 
 //ビルドしたものをプレビューする(ビルドでdistに出力したものを立ち上げる)http://localhost:5173/
 npm run preview 
```


### 不要ファイル削除
- src/App.css
- src/index.css

### コード編集

```tsx:src/App.tsx
export const App = () => {
  return <>ddd</>;
};
```

```tsx:src/main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import { App } from "./App";


ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

```


### Vitestのインストール
https://vitest.dev/

```:terminal
 npm i -D vitest happy-dom @vitest/coverage-v8 @testing-library/react @testing-library/user-event @testing-library/jest-dom @types/jest
```


|パッケージ|内容|
|---|---|
|vitest|Jest互換のテストフレームワークで、ネイティブ ESM サポートしているのでトランスパイル不要|
|happy-dom|jsdomより速い、Node.js用の多くのWeb標準を実装したJavaScriptライブラリ|
|@vitest/coverage-v8|Vitestのカバレッジプロバイダー|
|@testing-library/react|Reactのテストコード作成のためのツール|
|@testing-library/user-event|ユーザーがブラウザを操作したときにブラウザで発生する実際のイベントをシミュレートする|
|@testing-library/jest-dom|Testing Libraryといっしょに使う、Jest用のDOM要素のマッチャーを提供するライブラリ|
|@types/jest|jestの型定義|




package.jsonへの追記
```json:package.json
   "scripts": {
     "test": "vitest",
     "test:watch": "vitest watch",
     "coverage": "vitest run --coverage"
   },
```

ファイルの新規作成
```ts:vitest.setup.ts
import "@testing-library/jest-dom/vitest";
```

```tsx:App.tsx
import "@testing-library/jest-dom";
export const App = () => {
  return <></>;
};
```


```ts:vite.config.ts
 /// <reference types="vitest"/>
 import { defineConfig } from 'vite'
 import react from '@vitejs/plugin-react-swc'
 
 // https://vitejs.dev/config/
 export default defineConfig({
   plugins: [react()],
   test: {
     globals: true,
     environment: "happy-dom",
     setupFiles: ["./vitest.setup.ts"]
   }
 })
 ```

 ## テスト



### 足し算の検証

2つの数字を足す関数を作り、テストしてみます
 
#### 関数
```ts:src/calcs/Sum/Sum.ts
export const addition = (firstNumber: number, secondNumber: number): number => {
    return firstNumber + secondNumber;
  };
 
export const  division = (dividend: number, divisor: number): number => {
    return dividend / divisor;
  }
 ```

#### テスト
```ts:src/calcs/Sum/Sum.test.ts
import { addition, division } from "./Sum";

test("1+2が3になる", () => {
  expect(addition(1, 2)).toBe(3);
});

test("10/2が5になる", () => {
  expect(division(10, 2)).toBe(5);
});
```
※testではなくitと書くことも可能です
 

#### テスト実行
```terminal:terminal
 npm run test src/calcs/Sum/Sum.test.ts
 
 Test Files  1 passed (1)
      Tests  2 passed (2)
````
成功しましたが、本当に検証できているか確かめるためにわざと失敗してみます

```ts:src/calc/Sum/Sum.test.ts
test("1+3が3になる", () => {
  expect(sum(1, 3)).toBe(3);
})
``` 


#### テスト実行
```terminal:terminal
npm run test src/calc/Sum/Sum.test.ts

Test Files  1 failed (1)
     Tests  2 failed (2)
```
ちゃんと失敗しました。




### マッチャーの紹介
なんの説明もせずに「toBe」とか使ってしまいましたが、このようなものをマッチャーと呼びます

|等しいことを検証するマッチャー|説明|
|---|---|
|toEqual|等しい(number, string)|
|toBe|等しい(number, stringobject,) ※インタンスの参照まで確認する|

|数値の大小を検証するマッチャー|説明|
|---|---|
|toBeGreaterThan|より大きい|
|toBeGreaterThanOrEqual|以上|
|toBeLessThan|未満|
|toBeLessThanOrEqual|以下|
|toEqual|等しい|


|文字列を検証するマッチャー|説明|
|---|---|
|toContain|部分一致|
|toMatch|正規表現|
|toHaveLength|文字列の長さ|
|stringContaining|オブジェクトに含まれる文字列の検証(文字列で指定)|
|stringMatching|オブジェクトに含まれる文字列の検証(正規表現で指定)|

|真偽値を検証するマッチャー|説明|
|---|---|
|toBeTruthy|真であることを検証|
|toBeFalthy|偽であることを検証(null、undefinedはtoBeFalthyに一致するが、厳密に検証するならtoBeNull、toBeUndefinedを使う)|

|配列を検証するマッチャー|説明|
|---|---|
|toContain|配列に特定のプリミティブが含まれているかの検証|
|toHaveLength|配列の要素数の検証|
|toContainEqual|配列に特定のオブジェクトが含まれているかの検証|
|arrayContaining|配列に特定のオブジェクトが含まれているかの検証(引数に与えた配列要素が全て含まれているとテスト成功)|

|オブジェクトを検証するマッチャー||
|---|---|
|toMatchObject|プロパティが部分的に一致していればテストは成功する(一致しないプロパティがある場合は失敗)|
|toHaveProperty|特定のプロパティが存在するかの検証|
|objectContaining|オブジェクトに含まれるオブジェクトの検証(対象のプロパティが期待値のオブジェクトと部分的に一致すればテスト成功|


|null、undefined、NaNを検証するマッチャー|説明|
|---|---|
|toBeNull|nullであるかの検証|
|toBeUndefined|undefinedであるかの検証|
|toBeDefined|定義されているか(undefinedでないか)の検証|
|toBeNaN|NaNであるかの検証|


|例外処理の検証|説明|
|---|---|
|toThrow|関数が例外をスローするの検証(例外の種類はノーチェック)|
|toThrowError|関数が例外をスローするの検証(例外の種類もチェック)|


マッチャーの前にnotをつけると判定を反転できます
```ts:ts
not.マッチャー
```






### 例外処理(エラー)の検証
3種類の例外処理の検証をしてみます
- JavaScript の最大整数値は Number. MAX_SAFE_INTEGER という定数で定義されており、その値は 9007199254740991 
- JavaScript の最小整数値は Number. MIN_SAFE_INTEGER という定数で定義されており、その値は -9007199254740991
- 0で割ることはできない


```ts:src/calcs/MinMax/MinMax.ts
export const addition = (firstNumber: number, secondNumber: number): number | Error => {
  const result = firstNumber + secondNumber;
  if (result > Number.MAX_SAFE_INTEGER) {
    throw new Error("計算結果がJavaScriptで扱うには大きすぎます")
  }
  if (result < Number.MIN_SAFE_INTEGER) {
    throw new Error("計算結果がJavaScriptで扱うには小さすぎます")
  }
  return result;
};
export const division = (dividend: number, divisor: number): number | Error => {
  if (divisor === 0) {
    throw new Error("0で割ることはできません")
  }
  return dividend / divisor;
}
``` 
 


```ts:src/calcs/MinMax/MinMax.test.ts
import { addition, division } from "./MinMax";
 
describe("足し算のテスト", () => {
  test("1+2が3になる", () => {
    expect(addition(1, 2)).toBe(3);
  });
 
  test("計算結果がJavaScriptで扱うには小さすぎる場合にエラーを出す", () => {
    expect(() => addition(-9007199254740990, -2)).toThrow("計算結果がJavaScriptで扱うには小さすぎます");
  });
  test("計算結果がJavaScriptで扱うには大きすぎる場合にエラーを出す", () => {
    expect(() => addition(9007199254740990, 2)).toThrow("計算結果がJavaScriptで扱うには大きすぎます");
  });
 });
 
describe("割り算のテスト", () => {
  test("10/2が5になる", () => {
    expect(division(10, 2)).toBe(5);
  });
 
  test("10/2が5になる", () => {
    expect(() => division(10, 0)).toThrow("0で割ることはできません");
  });
});
``` 
※テストはdescribe関数でグルーピング可能です
(ネストも可能)
 
#### テスト実行
```terminal:terminal
npm test src/calcs/MinMax/MinMax.test.ts
 
Test Files  1 passed (1)
     Tests  5 passed (5)
```



### button要素の検証

```tsx:src/components/TestButton/TestButton.tsx
import { FC } from "react";

type testButtonProps = {
  label: string;
  onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
  disabled: boolean;
  type: "button" | "reset" | "submit";
};

export const TestButton: FC<testButtonProps> = ({
  label,
  onClick,
  disabled,
}) => {
  return (
    <div>
      <button type='button' onClick={onClick} disabled={disabled}>
        {label}
      </button>
    </div>
  );
};
``` 
 

```tsx:src/components/TestButton/TestButton.test.tsx
import { render, screen } from "@testing-library/react";
import "@testing-library/jest-dom/vitest";
import { TestButton } from "./TestButton";

test("ボタンが表示されている", () => {
  const handleButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
    alert(`${event.currentTarget.type}を押しました。`);
  };
  
  render(
    <TestButton
      label='Yes'
      type={"button"}
      onClick={handleButtonClick}
      disabled={false}
    />
  );
  const testElement = screen.getByRole("button");
  expect(testElement).toBeInTheDocument();
});
 
test("ボタンのラベルの文字が「Yes」", () => {
  const handleButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
    alert(`${event.currentTarget.type}を押しました。`);
  };
 
  render(
    <TestButton
      label='Yes'
      type={"button"}
      onClick={handleButtonClick}
      disabled={false}
    />
  );
  const testElement2 = screen.getByText("Yes");
  expect(testElement2).toBeInTheDocument();
});
 
test("2つのボタンが表示されている", () => {
  const handleButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
    const buttonElement = event.target as HTMLButtonElement;
    const buttonText = buttonElement.innerText;
    alert(`${buttonText}ボタンが押されました`);
  };

  render(
    <>
      <TestButton
        label='Yes'
        type={"button"}
        onClick={handleButtonClick}
        disabled={false}
      />
      <TestButton
        label='No'
        type={"button"}
        onClick={handleButtonClick}
        disabled={false}
     />
  </>
);
 
  //buttonロールで、アクセシブルネームが「Yes」のものを変数testElement1に入れる
  const testElement1 = screen.getByRole("button", { name: "Yes" });
  //buttonロールで、アクセシブルネームが「No」のものを変数testElement2に入れる
  const testElement2 = screen.getByRole("button", { name: "No" });
 
  //testElement1とtestElement2がドキュメント内にあることを検証する
  expect(testElement1 && testElement2).toBeInTheDocument();
});
 
test("ボタンのラベルの文字が「Yes」と「No」", () => {
  const handleButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
    alert(`${event.currentTarget.type}を押しました。`);
   };
 
  render(
    <>
      <TestButton
        label='Yes'
        type={"button"}
        onClick={handleButtonClick}
        disabled={false}
      />
      <TestButton
        label='No'
        type={"button"}
        onClick={handleButtonClick}
        disabled={false}
      />
    </>
  );
 
  //テキストが「Yes」の要素を変数testElement1に入れる
  const testElement1 = screen.getByText("Yes");
  //テキストが「Yes」の要素を変数testElement1に入れる
  const testElement2 = screen.getByText("No");
 
  //testElement1とtestElement2がドキュメント内にあることを検証する
  expect(testElement1 && testElement2).toBeInTheDocument();
});
```
 

### input要素(type=text)の検証

#### コンポーネント
```tsx:src/components/TestTextbox/TestTextbox.tsx
import { FC } from "react";
 
type testTextboxProps = {
  labelword: string;
  pairword: string;
  type: "text" | "password" | "search" | "email" | "url" | "number";
};

export const Textbox: FC<testTextboxProps> = ({
  labelword,
  pairword,
  type,
}) => {
  return (
    <>
      <label htmlFor={pairword}>{labelword}</label>
      <input type={type} id={pairword} />
    </>
  );
};
``` 
 
 

#### テストコード
```tsx:src/components/TestTextbox/TestTextbox.test.tsx
import { render, screen } from "@testing-library/react";
import "@testing-library/jest-dom/vitest";
import { Textbox } from "./TestTextbox";

test("テキストボックスが表示されている1", () => {
  //Textboxコンポーネントをレンダリングする
  render(<Textbox labelword='名前' pairword='aaa' type='text' />);
 
  //ロールがtextboxの要素を取得して変数textboxElementにいれる
  const textboxElement = screen.getByRole("textbox");
 
  //ドキュメント内にtextboxElementが存在することを検証
  expect(textboxElement).toBeInTheDocument();
});

test("テキストボックスが表示されている2", () => {
  //Textboxコンポーネントをレンダリングする
  render(<Textbox labelword='名前' pairword='aaa' type='text' />);
  screen.debug();
  //ロールがtextboxの要素を取得して変数textboxElementにいれる
  const textboxElement = screen.getByLabelText("名前");
 
  //ドキュメント内にtextboxElementが存在することを検証
  expect(textboxElement).toBeInTheDocument();
});
``` 
 

#### テスト実行
```terminal:terminal
npm run test src/components/TestTextbox/TestTextbox.test.tsx

Test Files  1 passed (1)
     Tests  1 passed (1)
```




### Heading要素の検証

#### コンポーネント1
```tsx:src/components/TestHeading/TestHeading1.tsx
import { FC } from "react";

type testHeadingProps = {
  text: string;
};

export const TestHeading1: FC<testHeadingProps> = ({ text }) => {
  return (
    <>
      <h1>{text}</h1>
    </>
  );
};
``` 
 
#### コンポーネント2
```tsx:src/components/TestHeading/TestHeading2.tsx
import { FC } from "react";

type testHeadingProps = {
  text: string;
};

export const TestHeading2: FC<testHeadingProps> = ({ text }) => {
  return (
    <>
      <h2>{text}</h2>
    </>
  );
};
``` 
 

#### テストコード
```tsx:src/components/TestHeading/TestHeading.test.tsx
import { render, screen } from "@testing-library/react";
import "@testing-library/jest-dom/vitest";
import { TestHeading1 } from "./TestHeading1";
import { TestHeading2 } from "./TestHeading2";

test("h1要素が表示されている", () => {
  render(<TestHeading1 text='rrrrrrrrrrrr' />);
  const testElement = screen.getByRole("heading");
  expect(testElement).toBeInTheDocument();
});
 
test("h2要素が表示されている", () => {
  render(<TestHeading2 text='rrrrrrrrrrrr' />);
  const testElement = screen.getByRole("heading");
  expect(testElement).toBeInTheDocument();
});
 
test("h1要素が表示されている(h1とh2が表示されている状態でh1を選別)", () => {
  render(
    <>
      <TestHeading1 text='rrrrrrrrrrrr' />
      <TestHeading2 text='rrrrrrrrrrrr' />
    </>
  );
  const testElement = screen.getByRole("heading", { level: 1 });
  expect(testElement).toBeInTheDocument();
});
``` 

#### テスト実行
```terminal:terminal
npm run test src/components/TestHeading/TestHeading.test.tsx

Test Files  1 passed (1)
     Tests  3 passed (3)
```

### DOM関連のマッチャー

|DOM関連のマッチャー|説明|
|---|---|
|toBeDisabled|disabledであることを検証|
|toBeEnabled|disabledでないことを検証|
|toBeEmptyDOMElement|DOMの中身が空であることを検証|
|toBeInTheDocument|DOMの中身が空でないことを検証|
|toBeInvalid|input要素などがinvalidか検証|
|toBeRequired|input要素などrequiresか検証|
|toBeValid|input要素などがinvalidではないか検証|
|toBeVisible|要素が目に見える状態か検証|
|toContainElement|要素を子要素として持っているか検証|
|toContainHTML|要素を子要素として持っているか検証|
|toHaveClass|対象のクラスを持っているか検証|
|toHaveFocus|focus状態であることを検証|
|toHaveFormValues|formの値を検証|
|toHaveStyle|対象のstyleを検証|
|toHaveTextContent|中の文字要素を検証|
|toHaveValue|inputなどのvalueを検証|
|toBeChecked|checkedであることを検証|





### WAI-ARIAのrole属性一覧


|HTML要素|暗黙のロール|備考|
|---|---|---|
|`<article>`|artilcle||
|`<aside>`|complementary||
|`<nav>`|navigation||
|`<header>`|banner||
|`<footer>`|contentinfo||	
|`<main>`|main||
|`<section>`|region|aria-labelledbyが指定されている場合のみ|
|`<form>`|form|アクセシブルネームを持つ場合のみ|
|`<button>`|button||
|`<a href=xxxxx">`|link|href属性を持つ場合のみ|
|`<input type="checkbox">`|checkbox||
|`<input type="radio">`|radio||
|`<input type="button">`|button||
|`<input type="text">`|textbox	
|`<input type="password">`|なし||
|`<input type="search">`|searchbox||	
|`<input type="email">`|textbox||
|`<input type="url">`|textbox||
|`<input type="tel">`|textbox||
|`<input type="number">`|spinbutton||	
|`<input type="range">`|slider||
|`<select>`|listbox||	
|`<optgroup>`|group||	
|`<option>`|option||	
|`<ul>`|list||	
|`<li>`|list||
|`<table>`|table||	
|`<caption>`|caption||
|`<th>`|columnheader/rowheader|列ヘッダーか、行ヘッダーかによる|
|`<td>`|cell||
|`<tr>`|row||
|`<fieldset>`|group||	
|`<legend>`|なし||
|`<h1>`|heading||
|`<h2>`|heading||
|`<h3>`|heading||
|`<h4>`|heading||
|`<h5>`|heading||
|`<h6>`|heading||


### クエリ

#### クエリのおおまかな分類
|種類|説明|
|--|--|
|getBy|見つからなければエラーが返される|
|queryBy|見つからなければnullが返される|
|findBy|非同期関数に使用することができる|

※~ByAll　対象の要素を全て取得することができる


#### getByの優先順位
|分類|優先順位|getBy|説明|
|---|---|---|---|
|誰でもアクセスできるクエリ|1|getByRole|ロール(役割)を判断する|
|誰でもアクセスできるクエリ|2|getByLabelText|ラベルだけ(役割は判断しない)|
|誰でもアクセスできるクエリ|3|getByPlaceholderText||
|誰でもアクセスできるクエリ|4|getByText|`<div>`、`<span>`、`<p>`など|
|誰でもアクセスできるクエリ|5|getByDisplayValue||
|セマンティッククエリ|6|getByAltText|要素のalt属性|
|セマンティッククエリ|7|getByTitle||
|テストID|8|getByTestId|	|


## テストをしやすくするツール

### テストのデバッグ
その時点のHTMLを確認できます
```tsx:tsx
screen.debug()
```



### Roleを出力する
下記を表示できます
- roleごとの要素一覧
- nameの値

```:tsx
 import { logRoles } from '@testing-library/dom'

logRoles(screen.getByRole('ロール名'))
```

### testing playground

https://testing-playground.com/

Chrome拡張をインストールする必要があります

https://chromewebstore.google.com/detail/testing-playground/hejbmebodbijjdhflfknehhcgaklhano

ブラウザに表示する必要があるため、App.tsxに調べたいコンポーネントを配置してからサーバーを起動します
```:terminal
npm run dev
```

1.Chromeのdevツールの「Element」「console」の並びの一番右の「>>」をクリック
2.「Testing Playground」を選択
3.左上の「select element」のアイコンをクリックしてから、ページの要素をホバーするとクエリの候補が確認できる(suggested queryの部分からコピーもできる)