TypeScriptとJestで始める！安全なテスト生活

この記事はTSKaigi Advent Calendar 2024 22日目の記事です。

https://qiita.com/advent-calendar/2024/tskaigi

## はじめに
こんにちは！TSKaigiクリエイティブチームの添田です！
2025年5月のTSKaigiの開催が待ち遠しくてワクワクが止まりません！

最近、TypeScriptとJestの組み合わせにすっかりハマっています。
皆さんにもその魅力を伝えたいと思います！😊

## なぜテストが必要なの？
コードを書いていると、「動作確認はブラウザで十分じゃない？」と思うかもしれません。
でも、自動テストがあると以下のような場面で頼もしい存在になります！

- ✨ 新機能の追加：既存の機能が壊れていないかすぐ確認できる
- 🐛 バグ修正後：同じバグが再発していないかチェック可能
- 👥 チーム開発：他メンバーが修正したコードの動作も保証される

## Jestって何がスゴイの？
JestはJavaScript向けの人気テストフレームワークです！

https://jestjs.io/ja/

- 🚀 簡単な書き方：テストの記述がシンプル
- 👀 見やすい結果表示：エラー箇所が一目でわかる
- 💪 TypeScript対応：ts-jestを使えばTypeScriptコードもそのままテスト可能

## 下準備
以下のコマンドでTypeScriptとJestをインストールします。

```
npm install --save-dev jest typescript ts-jest @types/jest
```

## 実践
### 足し算関数のテスト
まず、シンプルな足し算関数を用意します。

```typescript
export function sum(a: number, b: number): number {
  return a + b;
}
```

次に、この関数のテストコードを作成します。

最初にテスト対象となるsum関数をimportします。
```typescript
import { sum } from "./sum";
```

it関数はtest関数と同じ動作をしますが、好みに応じて使い分けが可能です。
当記事はit関数を使用しています。

it関数の第１引数は文字列でテストケースの名前を書きます。
英語はもちろん、日本語でも書くことができます。

第２引数はアロー関数を指定し、アロー関数の中にテストを書いていきます！

```typescript
it("3と2を足すと5になる", () => {});
```

expect関数を使い、引数にテスト対象の実行結果を渡します。
このtoBeはMatcher関数と呼ばれているもので、期待値を渡すものです。
```sum.test.ts
it("3と2を足すと5になる", () => {
 expect(sum(3, 2)).toBe(5);
});
```

Matcher関数については、他にもあるので、
興味ある方は是非以下を参考にしてみてください。

https://jestjs.io/ja/docs/using-matchers

### テストを実行
```terminal
npm test src/sum.test.ts
```
成功時：✅ 緑色のマーク
失敗時：❌ 赤色のマーク
見やすい出力で、どこが問題なのかすぐに分かりますね！
#### 成功時
```terminal
> jest-lesson@1.0.0 test
> jest src/sum.test.ts

 PASS  src/sum.test.ts
  ✓ 3と2を足すと5になる (1 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.102 s, estimated 2 s
```
#### 失敗時
```terminal
 FAIL  src/sum.test.ts
  ✕ 3と2を足すと5になる (2 ms)

  ● 3と2を足すと5になる

    expect(received).toBe(expected) // Object.is equality

    Expected: 8
    Received: 5

      2 |
      3 | it("3と2を足すと5になる", () => {
    > 4 |     expect(sum(3, 2)).toBe(8);
        |                       ^
      5 | });
      6 |

      at Object.<anonymous> (src/sum.test.ts:4:23)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        1.116 s, estimated 2 s
```

このエラーメッセージでは、Expected: 8とReceived: 5が異なるため、テストの期待値が正しいか、または関数が正しい値を返しているかを確認しましょう。
```typescript
expect(sum(3, 2)).toBe(8);
```

TypeScriptと組み合わせることで、テストコードそのものも型安全になります。
そのため、テストの記述時に型エラーを防ぐことができ、より信頼性の高いテストが書けます。

### ショッピングリストのテストをしてみよう
```typescript
/**
 * 【演習】
 *  1. `addItem`メソッドが、アイテムをリストに追加できることを確認するテストケース
 *  2. `removeItem`メソッドが、アイテムをリストから削除できることを確認するテストケース
 *  3. `removeItem`メソッドが、存在しないアイテムの削除を試みたときにエラーをスローすることを確認するテストケース
 */

export class ShoppingList {
  public list: string[];

  constructor() {
    this.list = [];
  }

  addItem(item: string): void {
    this.list.push(item);
  }

  removeItem(item: string): void {
    const index = this.list.indexOf(item);
    if (index === -1) {
      throw new Error(`アイテム: ${item} は存在しません`);
    }
    this.list.splice(index, 1);
  }
}
```

### 回答例
```typescript
import { ShoppingList } from "./practice"

describe("演習問題", () => {
    let shoppingList: ShoppingList;

    beforeEach(() => {
        // 各テストケースの実行前に新しいShoppingListインスタンスを作成します。
        shoppingList = new ShoppingList();
    });

    describe("addItem", () => {
        it("1. `addItem`メソッドが、アイテムをリストに追加できることを確認するテストケース", () => {
            shoppingList.addItem("メロン");
            expect(shoppingList.list).toHaveLength(1);  // 配列や文字列のサイズを確認
            expect(shoppingList.list[0]).toBe("メロン");  // 0番目の文字は「メロン」か確認
        })

        it("リストに複数アイテムを追加できる", () => {  // 追加
            shoppingList.addItem("リンゴ");
            shoppingList.addItem("ミカン");
            expect(shoppingList.list).toHaveLength(2);  // 配列や文字列のサイズを確認
            expect(shoppingList.list).toEqual(["リンゴ", "ミカン"]);    // objectを比較するのでtoEqual
        });
    });

    describe("removeItem", () => {
        it("2. `removeItem`メソッドが、アイテムをリストから削除できることを確認するテストケース", () => {
            shoppingList.addItem("メロン");
            shoppingList.addItem("リンゴ");
            shoppingList.removeItem("リンゴ");
            expect(shoppingList.list).not.toContain("リンゴ");  // アイテムが配列内にないことを確認
            expect(shoppingList.list).toHaveLength(1);  // 配列や文字列のサイズを確認
            expect(shoppingList.list[0]).toBe("メロン");  // 0番目の文字は「メロン」か確認
        })

        it("3. `removeItem`メソッドが、存在しないアイテムの削除を試みたときにエラーをスローすることを確認するテストケース", () => {
            // エラーがスローされることを確認
            expect(() => {
                shoppingList.removeItem("リンゴ");
            }).toThrow("アイテム: リンゴ は存在しません");
        })
    });
})
```

## テストを書くときのコツ📝
1. テストケースは具体的に書く
    - 「〜の場合、〜になること」という形式で書くと分かりやすい
    - 一つのテストでは一つの機能だけを確認する
2. beforeEachを活用
    - テストの前準備はbeforeEachでまとめると便利
    - テストケース間の独立性が保てる
3. エラーケースも忘れずに
    - 正常系だけでなく、異常系のテストも大切
    - ユーザーの予期せぬ操作にも対応できる

## さらなる一歩へ！
Jestには他にもたくさんの機能があります

1. 🎭 モック（Mock）を使ったテスト
2. 📸 スナップショットテスト
3. 📊 テストカバレッジの計測

## まとめ
今回は、TypeScriptとJestを使った基本的なテストの書き方を学びました！
はじめは少し難しく感じるかもしれませんが、テストを書くことで

- 💗コードの品質が上がる
- 💗バグの早期発見ができる
- 💗安心してリファクタリングできる

といった素晴らしいメリットがあります。
ぜひ、みなさんも実践してみてください！

## 📅イベント情報📅
TSKaigi2025が2025年5月23日/24日に開催されます！🎪
日本最大級のTypeScriptカンファレンスで、前回は2000人以上の方が参加されました！👥
TypeScriptに興味のある方は、ぜひチェックしてください！✨

TSKaigi 2025 ティザーサイト：https://2025.tskaigi.org/
公式サイト：https://tskaigi.org/
X：https://x.com/tskaigi

## 参考にさせていただいた資料
Jest公式ドキュメント：https://jestjs.io/ja/
TypeScript公式ドキュメント：https://www.typescriptlang.org/
Udemy教材：https://www.udemy.com/course/ts-webapp-test/?couponCode=ST21MT121624