Vitestで行う単体テストの基礎 – 条件分岐を全て網羅するテストの書き方
単体テストとは、ソフトウェアを構成する最小単位(関数やクラスなど)の動作を検証するテストである。
本記事では、JavaScript/TypeScript向けの軽量テストランナー「Vitest」を用いて、条件分岐の全てのパターンをテストする方法を示す。
また、カバレッジ(Coverage)の計測方法やVitestのセットアップ手順も紹介する。
目次
Vitestのセットアップ方法
まずはプロジェクトの初期化とVitestのインストールを行う。
npm init -y
npm install -D vitest
TypeScriptを利用する場合は、tsconfig.json
を作成し、ESMモジュールの設定などを行うとよい。また、package.json
には以下のようにテストスクリプトを追加する。
{
"scripts": {
"test": "vitest"
}
}
次に、Vitestの設定を行うために 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
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
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
を使う設定を示す。
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,
},
},
});
カバレッジを計測するには、以下のコマンドを実行する。
npm run test -- --coverage
coverage
ディレクトリが生成され、lcov-report
や index.html
をブラウザで開くと、どの行がテストされたかが可視化される。
ただし、カバレッジはあくまで一つの指標に過ぎず、数値を高くすること自体が目的ではない。
コード全体の品質は、条件分岐やエッジケースを漏れなくテストするかどうかに大きく左右される。
数値は参考程度にしつつ、すべての分岐を網羅できているかをチェックする方が品質向上に寄与する。
ベストプラクティス
-
テスト対象ごとにテストファイルを分割する
コードとテストを同じ階層(例:src/foo
とtest/foo
)で管理すると可読性が高まる。 -
条件分岐を明確にテストケースへ反映する
単純条件の場合は「真になるケース」「偽になるケース」、複数条件の場合はパターンを洗い出し、テストを分割する。 -
境界値を積極的にテストする
0
や負数、極端に大きい数など、境界となりやすい値をテストすることで予期せぬバグを防ぐ。 -
テストの粒度を小さく保ち、可読性を高める
1つのテストに過度に詰め込みすぎないようにし、それぞれの目的やパターンごとにテストを分ける。 -
カバレッジは参考程度に
カバレッジを測定して数値を追うよりも、条件分岐や異常系を含めたテストを網羅的に書く方が高い品質を維持しやすい。
まとめ
単体テストを書く際には、関数のあらゆる条件分岐を意識してテストケースを作成することが肝要である。特に、単純条件、複数条件、switch文、三項演算子、早期returnなど、コード中で想定されるあらゆるパターンを網羅的にテストすることが重要である。
Vitestは軽量かつ高速に動作し、Jestライクな書き方で扱いやすいので、フロントエンド・バックエンド問わずTypeScript/JavaScriptのプロジェクトに導入しやすい。加えて、カバレッジ機能を備えているが、それを機械的に追求するよりも
すべての条件分岐を漏れなくテストすることを優先するとよい。
ぜひ、今回紹介したテスト方針とセットアップ方法を取り入れ、安定したコード品質を保ちながらプロジェクトを進めてほしい。