Vitestで行う単体テストの基礎


# Vitestで行う単体テストの基礎 – 条件分岐を全て網羅するテストの書き方

単体テストとは、ソフトウェアを構成する最小単位（関数やクラスなど）の動作を検証するテストである。  
本記事では、JavaScript/TypeScript向けの軽量テストランナー「Vitest」を用いて、条件分岐の全てのパターンをテストする方法を示す。  
また、カバレッジ（Coverage）の計測方法やVitestのセットアップ手順も紹介する。

---

## 目次

1. [Vitestのセットアップ方法](#vitestのセットアップ方法)
2. [様々な条件分岐のテスト設計](#様々な条件分岐のテスト設計)
    1. [単純条件（if文）](#単純条件if文)
    2. [複数条件（if-else複数）](#複数条件if-else複数)
    3. [switch文](#switch文)
    4. [三項演算子（?:）](#三項演算子)
    5. [早期returnの分岐](#早期returnの分岐)
3. [サンプルコード](#サンプルコード)
    - [テスト対象: src/foo/bar.ts](#テスト対象-srcfoobarts)
    - [テスト: test/foo/bar.test.ts](#テスト-testfoobartestts)
4. [カバレッジ計測について](#カバレッジ計測について)
5. [ベストプラクティス](#ベストプラクティス)
6. [まとめ](#まとめ)

---

## Vitestのセットアップ方法

まずはプロジェクトの初期化とVitestのインストールを行う。

```bash
npm init -y
npm install -D vitest
```

TypeScriptを利用する場合は、`tsconfig.json` を作成し、ESMモジュールの設定などを行うとよい。また、`package.json`
には以下のようにテストスクリプトを追加する。

```json:package.json
{
  "scripts": {
    "test": "vitest"
  }
}
```

次に、Vitestの設定を行うために `vitest.config.ts` などの設定ファイルを用意する。  
（設定ファイルがなくても動作するが、カバレッジなどをカスタマイズしたい場合は作成が推奨される。）

```typescript:vitest.config.ts
import {defineConfig} from 'vitest/config'

export default defineConfig({
    test: {
// ここにテスト周りの設定を書く
    },
})
```

---

## 様々な条件分岐のテスト設計

### 単純条件（if文）

`if` 文で真偽を判定する場合、以下のようなパターンを考える必要がある。

- **真になるケース**
- **偽になるケース**

単一の条件分岐であれば、上記2パターンをテストすれば十分である。

### 複数条件（if-else複数）

`if-else` が複数回ネストしている場合や、複数の条件式が組み合わさっている場合は、以下のような観点を追加で考慮する。

- **それぞれの条件が真となるケース**
- **それぞれの条件が偽となるケース**
- **境界値や異常値などの特別な入力**

条件分岐が複雑になるほど、全パターンを意識してテストを整理することが重要である。

### switch文

`switch` 文を使うと、特定の値に応じて複数の処理が分岐する。以下の点を考慮する。

- **各caseがマッチするパターン**
- **defaultに入るパターン**
- **break漏れがないか**（テストを通じて動作確認する）

### 三項演算子（?:）

三項演算子は `条件 ? 値1 : 値2` のように書けるため、if文に比べて短く記述できるが、可読性の低下を招く場合もある。テストにおいては以下を確認する。

- **条件が真のとき**
- **条件が偽のとき**

### 早期returnの分岐

関数内で早期return（guard節）を利用する場合、処理が途中で終了するパターンと最後まで実行されるパターンの両方をテストする。

- **早期returnに引っかかるケース**
- **早期returnを通らずに処理が継続するケース**

---

## サンプルコード

以下に示すのは、様々な条件分岐を含む例である。

### テスト対象: src/foo/bar.ts

```typescript:src/foo/bar.ts
export function add(a: number, b: number): number {
    return a + b;
}

export function subtract(a: number, b: number): number {
    return a - b;
}

/**
 * 数値が0以上なら add を呼び出し、負なら subtract を呼び出す
 * （単純条件 - if文）
 */
export function conditionalCalc(a: number, b: number): number {
    if (a >= 0) {
        return add(a, b);
    } else {
        return subtract(a, b);
    }
}

/**
 * 数値 a, b, c を受け取り、以下のルールで加算する
 * - a と b が両方とも正なら a + b
 * - そうでなく、c が正なら b + c
 * - 上記以外はすべて subtract(a, b)
 * （複数条件 - if-else複数）
 */
export function multiConditionCalc(a: number, b: number, c: number): number {
    if (a > 0 && b > 0) {
        return add(a, b);
    } else if (c > 0) {
        return add(b, c);
    } else {
        return subtract(a, b);
    }
}

/**
 * 与えられた文字列 key の値によって異なる文字列を返す
 * （switch文）
 */
export function switchCalc(key: string): string {
    switch (key) {
        case 'foo':
            return 'FOO';
        case 'bar':
            return 'BAR';
        default:
            return 'UNKNOWN';
    }
}

/**
 * 3つの数値の中で最大の値を返す
 * 三項演算子を使用
 * （三項演算子）
 */
export function ternaryMax(a: number, b: number, c: number): number {
    // a と b の大きい方を firstMax とし、そこから c と比較
    const firstMax = a > b ? a : b;
    return c > firstMax ? c : firstMax;
}

/**
 * 早期returnを利用し、何かしらの条件を満たせばその場で終了
 * 引数が負数なら 0 を返す。それ以外の場合は加算結果を返す。
 * （早期return）
 */
export function earlyReturnCalc(a: number, b: number): number {
    if (a < 0 || b < 0) {
        return 0; // 早期return
    }
    return add(a, b);
}
  ```

### テスト: test/foo/bar.test.ts

```typescript:test/foo/bar.test.ts
import {describe, it, expect} from 'vitest'
import {
    add,
    subtract,
    conditionalCalc,
    multiConditionCalc,
    switchCalc,
    ternaryMax,
    earlyReturnCalc,
} from '../../src/foo/bar'

describe('bar.tsの単体テスト', () => {
    it('add関数のテスト', () => {
        expect(add(1, 2)).toBe(3);
        expect(add(-3, 5)).toBe(2);
    });

    it('subtract関数のテスト', () => {
        expect(subtract(5, 3)).toBe(2);
        expect(subtract(0, 1)).toBe(-1);
    });

    describe('conditionalCalc関数のテスト（単純条件）', () => {
        it('aが0以上の場合', () => {
            expect(conditionalCalc(2, 3)).toBe(5);
            expect(conditionalCalc(0, 3)).toBe(3); // a=0 の境界値
        });
        it('aが負の場合', () => {
            expect(conditionalCalc(-2, 3)).toBe(-5);
        });
    });

    describe('multiConditionCalc関数のテスト（複数条件）', () => {
        it('a, b ともに正', () => {
            expect(multiConditionCalc(1, 2, -1)).toBe(3);
        });
        it('c が正のケース', () => {
            expect(multiConditionCalc(-1, 2, 3)).toBe(5);
            expect(multiConditionCalc(0, -2, 5)).toBe(3);
        });
        it('いずれの条件にも当てはまらない場合', () => {
            expect(multiConditionCalc(-1, -1, 0)).toBe(-(-1 - -1));
            expect(multiConditionCalc(1, -3, -2)).toBe(-(1 - -3));
        });
    });

    describe('switchCalc関数のテスト（switch文）', () => {
        it('keyが"foo"の場合', () => {
            expect(switchCalc('foo')).toBe('FOO');
        });
        it('keyが"bar"の場合', () => {
            expect(switchCalc('bar')).toBe('BAR');
        });
        it('それ以外の場合', () => {
            expect(switchCalc('baz')).toBe('UNKNOWN');
            expect(switchCalc('')).toBe('UNKNOWN');
        });
    });

    describe('ternaryMax関数のテスト（三項演算子）', () => {
        it('aが最大', () => {
            expect(ternaryMax(5, 3, 1)).toBe(5);
        });
        it('bが最大', () => {
            expect(ternaryMax(3, 7, 1)).toBe(7);
        });
        it('cが最大', () => {
            expect(ternaryMax(1, 4, 9)).toBe(9);
        });
        it('複数が同じ値の場合', () => {
            expect(ternaryMax(5, 5, 5)).toBe(5);
            expect(ternaryMax(5, 5, 3)).toBe(5);
        });
    });

    describe('earlyReturnCalc関数のテスト（早期return）', () => {
        it('負数が含まれている場合は0を返す', () => {
            expect(earlyReturnCalc(-1, 5)).toBe(0);
            expect(earlyReturnCalc(5, -2)).toBe(0);
            expect(earlyReturnCalc(-1, -2)).toBe(0);
        });
        it('どちらも負数でない場合は加算結果を返す', () => {
            expect(earlyReturnCalc(1, 2)).toBe(3);
            expect(earlyReturnCalc(0, 3)).toBe(3);
        });
    });
});
```

---

## カバレッジ計測について

Vitestでは、設定ファイルに以下のような項目を追記することで、`istanbul` や `c8` を使用したカバレッジ計測が可能になる。  
例として `vitest.config.ts` にて `istanbul` を使う設定を示す。

```typescript:vitest.config.ts
import {defineConfig} from 'vitest/config'

export default defineConfig({
    test: {
        coverage: {
            provider: 'istanbul',
            reporter: ['text', 'lcov'],
            reportsDirectory: './coverage',
// 必要に応じて閾値設定も可能
// lines: 80,
// functions: 80,
// branches: 80,
// statements: 80,
        },
    },
});
```

カバレッジを計測するには、以下のコマンドを実行する。

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

`coverage`ディレクトリが生成され、`lcov-report` や `index.html` をブラウザで開くと、どの行がテストされたかが可視化される。

ただし、カバレッジはあくまで**一つの指標**に過ぎず、数値を高くすること自体が目的ではない。  
コード全体の品質は、条件分岐やエッジケースを漏れなくテストするかどうかに大きく左右される。  
数値は参考程度にしつつ、**すべての分岐を網羅できているかをチェックする**方が品質向上に寄与する。

---

## ベストプラクティス

1. **テスト対象ごとにテストファイルを分割する**  
   コードとテストを同じ階層（例：`src/foo` と `test/foo`）で管理すると可読性が高まる。

2. **条件分岐を明確にテストケースへ反映する**  
   単純条件の場合は「真になるケース」「偽になるケース」、複数条件の場合はパターンを洗い出し、テストを分割する。

3. **境界値を積極的にテストする**  
   `0` や負数、極端に大きい数など、境界となりやすい値をテストすることで予期せぬバグを防ぐ。

4. **テストの粒度を小さく保ち、可読性を高める**  
   1つのテストに過度に詰め込みすぎないようにし、それぞれの目的やパターンごとにテストを分ける。

5. **カバレッジは参考程度に**  
   カバレッジを測定して数値を追うよりも、**条件分岐や異常系を含めたテストを網羅的に書く**方が高い品質を維持しやすい。

---

## まとめ

単体テストを書く際には、関数のあらゆる条件分岐を意識してテストケースを作成することが肝要である。特に、単純条件、複数条件、switch文、三項演算子、早期returnなど、コード中で想定されるあらゆるパターンを網羅的にテストすることが重要である。

Vitestは軽量かつ高速に動作し、Jestライクな書き方で扱いやすいので、フロントエンド・バックエンド問わずTypeScript/JavaScriptのプロジェクトに導入しやすい。加えて、カバレッジ機能を備えているが、それを機械的に追求するよりも
**すべての条件分岐を漏れなくテストすること**を優先するとよい。

ぜひ、今回紹介したテスト方針とセットアップ方法を取り入れ、安定したコード品質を保ちながらプロジェクトを進めてほしい。