フロントエンドテストに入門した記事（備忘録）

### ⚠️テスト初心者未満の者が書いた記事なので間違えてたら教えてください

## 🧠 この記事を書く前の気持ち
* 実務でテストを書いたことがないけど就活大丈夫なのか、、
* フロントエンドのテストってまず何
* なんとなくだけでも知りたい
* ???????????????????????????????????

## 🔍 フロントエンドのテストってそもそもなぜ必要？
人によっていろんな考え方がありそう、、、🤔
（てかフロントのテストとか思想強い人いそう〜〜〜〜〜〜怖い）
以下のものは調べて納得したものたち
### ✅ ユーザーにはフロントしか見えていないから

バックエンドが完璧でも、ボタンが押せなかったら終わり。
以下のような不具合はフロントの責任  

- ボタンが押せない
- 入力しても反応がない
- エラーメッセージが出ない

### ✅ 人間のミスに気づいてくれる

機能が盛りだくさんのサービスとかだと以下のようなミスが確認不足で起こったりする。（私だけかも）

- API のレスポンスに新しいプロパティが追加されて、undefined のまま表示されるようになってた
- バリデーションの条件変えたときのテスト漏れ


### ✅ 再発防止になる（バグの歴史を記録できる）

不具合のたびにそれに対応するテスト書いとけば、同じバグを本番環境で繰り返さなくなる。
再発防止策を考えるのって難しいけどテストがしっかり書かれているプロジェクトだとそれがめちゃ楽そう。
テストを書くのは大変だろうけど、、、


### ✅ 長期的に開発スピードが上がる

本番環境に反映するのが不安で確認にものすごく時間をかけてしまう（しかも全然確認ミスする）から以下の二つめっっっっっっっっちゃでかいメリットだと思った。

- 手動確認が減る
- 修正に自信が持てる

### ✅ テストは「ドキュメント」っぽくなる（かも）

会社によるとは思うけど、小さい修正とかだとドキュメントに残さないことがあったりして後からここはなんだ！ってなることがある。
でもテストは書いて通らないとダメだから半強制的にドキュメントのようなものも残せていいと思った。


### 結論：テストは書いておくと、未来の自分を助けられる


## 🤖 テストの種類まとめ（単体・結合・E2E）

### ✅ 単体テスト（ユニットテスト）

1つの**部品（関数やコンポーネント）だけ**をテストする。

* 対象：関数やコンポーネント単体
* 目的：ロジックが意図通りに動いているかを確認する

```tsx:例：Buttonコンポーネントのクリックイベントが正しく動作するか確認する（jest）
test('Button がクリックされたら関数が呼ばれる', () => {
  // モック関数を作成
  const mockFn = jest.fn();
  // Buttonコンポーネントをレンダリング => onClick属性にモック関数を渡す
  render(<Button onClick={mockFn} />);
  // 画面上の「button」ロールを持つ要素を見つけてクリックイベントを発火させる
  fireEvent.click(screen.getByRole('button'));
  // モック関数が呼び出されたかどうかを検証する(=ボタンがクリックされたときにonClick関数が実行されたかを確認する)
  expect(mockFn).toHaveBeenCalled();
});
```

:::note info
**モック関数とは？**
テスト中に使う「ダミーの関数」

**いつ使う？**
・ この関数が呼ばれたかどうかだけ確認したいとき
・ 本物のAPIを呼び出さずに仮の処理を走らせたいとき

:::

### ✅ 結合テスト（インテグレーションテスト）

**複数の部品を組み合わせた状態**でテストする。  
フォーム入力 → ボタン押す → API呼ばれる みたいな流れ。

* 対象：複数のコンポーネント・関数の組み合わせ
* 目的：一連の流れがちゃんと機能しているかを確認する

```tsx:例：Formコンポーネントでユーザー名を入力して送信した時の動作を検証する（jest）
test('ユーザー名を入力して送信するとAPIが呼ばれる', async () => {
  // Formコンポーネントをレンダリング
  render(<Form />);
  
  // 「ユーザー名」というラベルがついた入力フィールドを見つける
  // その値を'momomin'に変更するイベントを発火
  // /ユーザー名/i の i は大文字小文字を区別しないという意味（今回はいらんか）
  fireEvent.change(screen.getByLabelText(/ユーザー名/i), {
    target: { value: 'momomin' },
  });
  
  // 「Submit」というテキストがついたボタンを見つけてクリックイベントを発火
  fireEvent.click(screen.getByRole('button', { name: /Submit/i }));
  
  // 非同期処理（API呼び出し）の完了を待つ
  await waitFor(() => {
    // モック化されたAPI関数が正しいパラメータ（ユーザー名 'momomin'）で呼び出されたことを検証
    expect(mockedApi).toHaveBeenCalledWith({ name: 'momomin' });
  });
});
```


### ✅ E2Eテスト（End to End）

**ユーザーが実際に操作するブラウザ上で、UI全体**をテストする。  
CypressやPlaywrightを使ってブラウザを自動操作。

* 対象：アプリ全体（ブラウザで動かす）
* 目的：実際のユーザーがやる操作をまるごと再現する

```bash:例：今回そんなに勉強していない（Cypress）
cy.visit('/login')
cy.get('input[name=email]').type('user@example.com')
cy.get('button').click()
cy.url().should('include', '/dashboard')
```

## 🎯 なにをテストして、なにをテストしないのか？

テスト書くのに時間かかりすぎたり、テストがただのめんどくさい過程になったら意味がない。
労力と見返りが合わなくなっちゃう🤦🏽

### ✅ テストすべき優先順位（個人的な基準）

| 優先度 | テスト対象の例 | 理由 |
|--------|----------------|------|
| ★★★   | ユーザー操作・送信・認証・決済など | 壊れたら致命的すぎる |
| ★★☆   | 状態管理や複雑なロジック | 見た目じゃ気づきにくいバグを防ぐ（確認漏れもなくなる） |
| ★☆☆   | シンプルな見た目だけの UI | スナップショット or テストしないこともある？ |


:::note info
**スナップショットとは？**
コンポーネントをレンダリングしたときの HTML構造を丸ごと保存しておくテスト
・UIが予期せず変更されていないかを確かめる（大きいプロジェクトほど影響範囲が大きいので確認漏れが起きやすい）

**何が起きる？**
1回目のテスト実行時に、こんな感じのDOMスナップが `.snap` ファイルに保存される👇

```html:.snap
<button class="primary" disabled>
  送信中...
</button>
```

その後誰かがコードを変えて

```tsx:コンポーネント.tsx
<button className="secondary">送信</button>
```

になってたら、テストが失敗して以下のように変更点を教えてくれる！

```diff
- <button class="primary" disabled>
-   送信中...
+ <button class="secondary">
+   送信する
```
この変更が意図的なら`.snap` ファイルを更新して、意図的でない変更だったら修正する

:::

### ✅ テストしすぎてしんどくなるパターン

- 実装の中身に依存しすぎるテスト（＝変更に弱くなる、テストの目的がずれていきそう）
- ロジックがコンポーネント内に詰まっていて、書きにくい
- UIの微調整が多い


### 🧘‍♀️ 私なりの落としどころ

→ 「全部テストしよう！」ってよりも、「**ここだけは絶対壊さない**」ってとこをテストちゃんと書くスタンス。（結局は会社の規則によるが）


## ⭐️ テストしやすいコード構造

**コンポーネントの役割を分ける（＝関数化）** ことの大切さ。

### ✅ テストしやすくなる分割の考え方

```bash
❌ 悪い例：
PostCard.tsx ← 表示・状態管理・ロジック・副作用が全部ここ

⭕️ 良い例：
PostCard.tsx         ← 見た目に集中
usePostCardLogic.ts ← ロジックや状態
```

- UI部分だけを単体でレンダリングしてテストできる
- ロジック部分は関数やカスタムフックとしてテストできる
- 不具合が起きても、どのレイヤーの問題か切り分けやすい


### ✅ よくある構成：プレゼンテーションとロジックを分ける

```tsx:PostCard.tsx（UIだけに集中）
export const PostCard = ({ title, onClick }: Props) => (
  <div>
    <h2>{title}</h2>
    <button onClick={onClick}>クリック</button>
  </div>
);
```

```ts:usePostCardLogic.ts（状態や処理）
export const usePostCardLogic = () => {
  const handleClick = () => {
    // なにかロジック処理
  };

  return { handleClick };
};
```


#### この構成だと…

- `PostCard.tsx` だけをレンダリングして「タイトル表示されてるか」「ボタン押せるか」みたいな**見た目チェックだけが簡単にできる**
- `usePostCardLogic.ts` だけを呼び出して、「handleClick の挙動が正しいか」みたいな**ロジックのテストだけが書ける**


## 📊 `jest --coverage` を使ってみる

書いたテストがどれくらいカバーできてるか見れる

### ✅ コマンド

```bash
npm run test -- --coverage
```

### ✅ 結果の見方（ターミナル）

```txt
----------------------------|---------|----------|---------|---------|
File                        | % Stmts | % Branch | % Funcs | % Lines |
----------------------------|---------|----------|---------|---------|
All files                   |   85.71 |    66.67 |   75.00 |   85.71 |
 src/components/Button.tsx  |   100.0 |      100 |     100 |     100 |
 src/utils/calc.ts          |      75 |       50 |      60 |      75 |
----------------------------|---------|----------|---------|---------|
```

| 指標 | 意味（ざっくり） |
|------|------|
| `% Stmts` | 実行されたコード全体のうち何%がテストで実行されたか|
| `% Branch` | if, switch, 三項演算子 (?) の分岐ごとの通過率 |
| `% Funcs` | 関数やメソッドごとの通過率 |
| `% Lines` | ファイル内の全行のうち、テストで実行された行の割合（行単位） |

### ✅ --coverageについての補足

- 分岐の片方だけしか通ってないことに気づける
(ifが真のときしかテストしてなかった=>偽のとき落ちる、みたいな事故を防げる)

- 1度も呼ばれていない関数やフックに気づける
（書いたけどどこからも呼ばれてない => 実行されてない => テストで漏れてる）


## ♿️ アクセシビリティとテスト

- UIが「見えてるだけ」じゃ不十分。**使えるかどうか**が重要
- スクリーンリーダーやキーボード操作でも動くUIを作るには、**ラベル・ロール・フォーカス**の設計が重要
- テストでそれを意識しておけば、自然とアクセシブルなUIになる


### ✅ テストコードの中で`a11yフレンドリー`なセレクターを使う

```tsx
// ❌: テキストやクラス名に依存している
screen.getByText('送信');
screen.getByTestId('submit-button');

// ⭕️: アクセシビリティに基づいた取得方法
screen.getByRole('button', { name: '送信' });
screen.getByLabelText('メールアドレス');
```

### ✅ 自動a11yチェック：`jest-axe` 

`jest-axe`は `axe-core` ベースのa11yチェッカー。

```bash
npm install --save-dev jest-axe
```

```tsx
import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);

test('アクセシビリティ違反がない', async () => {
  const { container } = render(<MyComponent />);
  const results = await axe(container);
  expect(results).toHaveNoViolations();
});
```

☑️ **色コントラスト不足**  
☑️ **label のない input**  
☑️ **roleのないボタン**  
みたいな基本的なa11yエラーを見つけてくれる。


### ✅ `logRoles()` で今のDOMに何のロールがあるか見る

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

test('ロールを確認', () => {
  const { container } = render(<MyComponent />);
  logRoles(container);
});
```

📋 `dialog` や `button`、`form` などの**アクセシビリティロール**が正しく振られてるかが分かる。


## 🔥 テスト入門〜Jest触ってみる編〜

### 💻 環境
* Next.js - 14.0.3
* React - 18系
* Node.js - 20.9.0

### ①JestとTesting Libraryのインストール

```bash
npm install --save-dev jest @testing-library/react @testing-library/jest-dom
```

### ②テストを書くファイル作成

以下のどっちかの名前で
```bash
ComponentName.test.tsx
ComponentName.spec.tsx
```
こんな記事もあった👇
[*.test.ts と *.spec.ts の使い分け](https://zenn.dev/takashiaihara/articles/b35532a9f96c0c)

### ③最小のテストファイルを書いてみる

🎯 目的：ボタンをクリックしたとき、渡された関数が呼ばれるかどうか

```tsx:Button.tsx（テストの対象）
export default function Button({ onClick }: { onClick: () => void }) {
  return <button onClick={onClick}>送信</button>;
}
```

```tsx:Button.test.tsx

import { render, screen, fireEvent } from '@testing-library/react';
import Button from './Button';

test('ボタンがクリックされたら関数が呼ばれる', () => {
  // ① 呼ばれた回数を確認したいからモック関数を使う
  const handleClick = jest.fn();

  // ② コンポーネントを描画してボタンを探す
  render(<Button onClick={handleClick} />);

  // ③ テキストが「送信」のボタンをクリック
  fireEvent.click(screen.getByText('送信'));

  // ④ モック関数が呼ばれたことを確認
  expect(handleClick).toHaveBeenCalled();
});

```

### ④スナップショットテスト

🎯 目的：見た目が勝手に変わってないかをチェックする

```tsx:Button.tsx（テストの対象）
export default function Button({ onClick }: { onClick: () => void }) {
  return <button onClick={onClick}>送信</button>;
}
```

```tsx:Button.test.tsx
import { render } from '@testing-library/react';
import Button from './Button';

test('スナップショットと一致する', () => {
  // ① asFragment() でHTML構造を取得
  const { asFragment } = render(<Button onClick={() => {}} />);

  // ② 取得した構造と過去のスナップショットファイルが一致するか比較
  expect(asFragment()).toMatchSnapshot();
});
```

### ⑤非同期処理（useEffectなど）

🎯 目的：非同期で状態が変わるUIがちゃんと更新されるか

```tsx:AsyncComponent.tsx（テストの対象）

import { useEffect, useState } from 'react';

export default function AsyncComponent() {
  const [loaded, setLoaded] = useState(false);

  useEffect(() => {
    setTimeout(() => setLoaded(true), 1000);
  }, []);

  return <div>{loaded ? '読み込み完了' : '読み込み中...'}</div>;
}

```

```tsx:AsyncComponent.test.tsx

import { render, screen, waitFor } from '@testing-library/react';
import AsyncComponent from './AsyncComponent';

test('非同期処理後にテキストが変わる', async () => {
  // ① 初期表示（読み込み中）
  render(<AsyncComponent />);

  // ② setTimeoutで状態が変わるのを待って「読み込み完了」 が表示されるか確認
  await waitFor(() => {
    expect(screen.getByText('読み込み完了')).toBeInTheDocument();
  });
});


```

### ⑥カスタムフックのテスト

🎯 目的：自作フックの状態がどう変わるか確認する

```bash
npm install --save-dev @testing-library/react-hooks
```

```tsx:useCounter.ts（テストの対象）
import { useState } from 'react';

export default function useCounter() {
  const [count, setCount] = useState(0);
  const increment = () => setCount((c) => c + 1);
  return { count, increment };
}
```

```tsx:useCounter.test.ts

import { renderHook } from '@testing-library/react-hooks';
import useCounter from './useCounter';

test('incrementでカウントが増える', () => {
  // ① フックをレンダリングして内部の値を取得
  const { result } = renderHook(() => useCounter());

  // ② 初期値を確認
  expect(result.current.count).toBe(0);

  // ③ increment を呼び出して、状態が変わるか確認
  result.current.increment();
  expect(result.current.count).toBe(1);
});

```

### ⑦よく使うやつ？まとめ（React Testing Library + Jest）

| メソッド                         | なにをする？                                           | メモ                                      |
|----------------------------------|--------------------------------------------------------|-----------------------------------------------------|
| `render()`                       | コンポーネントを仮想DOMに描画                          | まずこれで画面を表示する       |
| `screen.getByText('文字')`       | 指定したテキストを含む要素を探す                      |   テキストが「送信」のボタンを探す   |
| `fireEvent.click(要素)`          | 対象要素をクリック                                     | ユーザーの操作を再現する（ボタン押すとか）           |
| `jest.fn()`                      | モック関数を作る                                      | 関数が呼ばれたかどうか確認するとき          |
| `expect(value).toBe(x)`          | 値が x と一致するか確認               |     特になし    |
| `expect(fn).toHaveBeenCalled()`  | 関数が呼ばれたか確認                                   |          モック関数とセットでよく使う            |
| `waitFor(() => ...)`            | 非同期処理の完了を待ってからチェック              |   特になし   |


## 💎 テスト入門〜コーディングテストでテストを書いたヨ編〜

### ✅ コンポーネントのテストパターン

単純なコンポーネントのテストは以下の三つの種類が多かった。

```tsx
import { render, screen, fireEvent } from '@testing-library/react';
import YourComponent from './YourComponent';

describe('YourComponent', () => {
  // 1. レンダリングテスト - コンポーネントが正しく表示されるか
  it('正しくレンダリングされる', () => {
    render(<YourComponent />);
    expect(screen.getByText('期待する文字列')).toBeInTheDocument();
  });

  // 2. インタラクションテスト - ユーザーの操作に正しく反応するか
  it('ボタンクリックでイベントハンドラが呼ばれる', () => {
    const handleClick = jest.fn();
    render(<YourComponent onClick={handleClick} />);
    fireEvent.click(screen.getByRole('button'));
    expect(handleClick).toHaveBeenCalled();
  });

  // 3. 状態変化のテスト - 状態に応じた表示変化が正しいか
  it('状態変化で表示が切り替わる', () => {
    render(<YourComponent />);
    expect(screen.getByText('初期状態')).toBeInTheDocument();
    
    fireEvent.click(screen.getByRole('button'));
    expect(screen.getByText('変化後の状態')).toBeInTheDocument();
  });
});
```

### ✅ テスト時の要素の探し方

要素の探し方は全部getByRoleでいけるのが理想なのかな、、、
これは正直理想論なのかもしれないわからない。

```tsx
// 最優先（getByRole）
screen.getByRole('button', { name: 'ログイン' });
screen.getByLabelText('パスワード');

// 次点（getByText）
screen.getByText('ようこそ');
screen.getByPlaceholderText('例: user@example.com');

// 避けたい（GPTに聞いた）
screen.getByTestId('login-form');
```


### ✅ 動きのあるコンポーネント

```tsx:1. フォームのテスト
test('フォーム入力と送信が正しく動作する', async () => {
  // モックの準備
  const onSubmit = jest.fn();
  
  render(<LoginForm onSubmit={onSubmit} />);
  
  // 入力フィールドを操作
  fireEvent.change(screen.getByLabelText('メールアドレス'), {
    target: { value: 'test@example.com' }
  });
  
  fireEvent.change(screen.getByLabelText('パスワード'), {
    target: { value: 'password' }
  });
  
  // フォーム送信
  fireEvent.click(screen.getByRole('button', { name: 'ログイン' }));
  
  // 送信ハンドラが正しい値で呼ばれたか確認
  expect(onSubmit).toHaveBeenCalledWith({
    email: 'test@example.com',
    password: 'password'
  });
});
```

```tsx:条件分岐のあるUIのテスト
test('状態によって表示が切り替わる', () => {
  const { rerender } = render(<StatusDisplay status="loading" />);
  expect(screen.getByText('読み込み中')).toBeInTheDocument();
  
  // エラー状態
  rerender(<StatusDisplay status="error" message="接続エラー" />);
  expect(screen.getByText('エラー: 接続エラー')).toBeInTheDocument();
  
  // 成功状態
  rerender(<StatusDisplay status="success" data={{ name: '成功' }} />);
  expect(screen.getByText('成功')).toBeInTheDocument();
});
```

```tsx:モーダル・ポップアップのテスト
test('モーダルの表示・非表示が切り替わる', () => {
  render(<ModalExample />);
  
  // 初期状態ではモーダルは非表示
  expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
  
  // モーダルを開くボタンをクリック
  fireEvent.click(screen.getByRole('button', { name: 'モーダルを開く' }));
  
  // モーダルが表示される
  const modal = screen.getByRole('dialog');
  expect(modal).toBeInTheDocument();
  
  // モーダルを閉じるボタンをクリック
  fireEvent.click(screen.getByRole('button', { name: '閉じる' }));
  
  // モーダルが非表示になる
  expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
});
```

# ⭐️ 最後に

この記事は私がフロントエンドテストに入門した勉強記録です。
実務でテストを書く機会が来たらまた読み返しにきます。
もっと深く勉強するべきところとかあれば教えていただけるとありがたいです。
就活頑張るにょ🍑🍑