今回やること
Vitest 4で、次の3種類のユニットテストを書きます。
- 同じ入力に同じ結果を返す純粋関数
- Promiseを返すasync関数
vi.fnで作った偽物の依存関数
最後にテストを意図的に失敗させ、ログの「期待値」「実際値」「対象行」を読みます。React、ブラウザ、CIは使いません。
前提条件
- Node.js 20以上
- npm
- TypeScriptの関数と
async/awaitが読める
Vitest 4はNode.js 20以上が必要です。
node --version
npm --version
プロジェクトを準備する
mkdir vitest-practice
cd vitest-practice
npm init -y
npm install --save-dev vitest@4 typescript @types/node
mkdir src
package.jsonへtypeとtestスクリプトを追加します。devDependenciesはnpm installが作った内容を残してください。
{
"type": "module",
"scripts": {
"test": "vitest run",
"test:watch": "vitest"
}
}
vitest runは1回実行して終了します。vitestはファイル変更を待って再実行する開発用モードです。
テスト対象を作る
src/order.tsを作ります。
export type User = {
id: string;
name: string;
};
export function shippingFee(total: number): number {
return total >= 5_000 ? 0 : 500;
}
export async function greeting(
loadUser: () => Promise<User>,
): Promise<string> {
const user = await loadUser();
return `こんにちは、${user.name}さん`;
}
export function completeOrder(
orderId: string,
notify: (message: string) => void,
): void {
notify(`注文 ${orderId} を受け付けました`);
}
shippingFeeは外部状態を使わない純粋関数です。greetingとcompleteOrderは、データ取得や通知の処理を引数で受け取ります。この形なら、テストで本物のAPIやメールを動かす必要がありません。
テストを書く
src/order.test.tsを作ります。
import { describe, expect, it, vi } from "vitest";
import { completeOrder, greeting, shippingFee } from "./order";
describe("shippingFee", () => {
it("4,999円では送料500円になる", () => {
expect(shippingFee(4_999)).toBe(500);
});
it("5,000円から送料無料になる", () => {
expect(shippingFee(5_000)).toBe(0);
});
});
describe("greeting", () => {
it("asyncで取得した利用者名を使う", async () => {
const loadUser = vi.fn().mockResolvedValue({
id: "user-1",
name: "Ada",
});
await expect(greeting(loadUser)).resolves.toBe("こんにちは、Adaさん");
expect(loadUser).toHaveBeenCalledOnce();
});
});
describe("completeOrder", () => {
it("注文IDを含む通知を1回だけ送る", () => {
const notify = vi.fn();
completeOrder("order-1", notify);
expect(notify).toHaveBeenCalledOnce();
expect(notify).toHaveBeenCalledWith("注文 order-1 を受け付けました");
});
});
3種類の確認内容
shippingFeeでは、条件が切り替わる4,999円と5,000円を確認します。これを境界値テストと呼びます。
greetingではテスト関数をasyncにし、await expect(...).resolvesでPromiseの完了を待ちます。loadUserはvi.fn().mockResolvedValueで作った偽物です。
completeOrderでは通知本文だけでなく、呼び出し回数も確認します。vi.fnは実際にメールを送らず、どのように呼ばれたかを記録します。
成功を確認する
npm test
4件のテストが成功し、終了コードが0になれば成功です。
Test Files 1 passed
Tests 4 passed
表示時間などは環境によって異なります。
失敗ログを読む
src/order.test.tsの最初の期待値を、一時的に次のように変えます。
expect(shippingFee(4_999)).toBe(0);
もう一度npm testを実行すると、失敗ログに次の情報が出ます。
Expected: テストが期待した0Received: 実際に返った500- ファイル名と行番号: 失敗した
expectの場所
まず対象行を開き、「実装が間違っているのか」「期待値が間違っているのか」を仕様と照合します。この例では4,999円の送料は500円なので、期待値を500へ戻すのが正解です。
よくあるエラー
Cannot find package 'vitest'
プロジェクトのルートで依存関係を入れます。
npm install
No test files found
ファイル名が*.test.tsまたは*.spec.tsになっているか確認します。この記事ではsrc/order.test.tsです。
asyncテストが意図せず成功する
Promiseをreturnもawaitもしていない可能性があります。この記事のようにテスト関数へasyncを付け、await expect(...)まで書きます。
練習
shippingFeeへ「合計が負ならエラーを投げる」という仕様を追加し、次のテストが成功するよう実装してください。
expect(() => shippingFee(-1)).toThrow("total must be zero or greater");
次のステップ
同じテスト対象で、正常系だけでなく入力の端を選ぶ方法を境界値テストの作り方で確認してください。ブラウザ操作のテストはPlaywright E2Eテストで別に扱います。
参考リソース
- Vitest: Getting Started
- Vitest: Testing asynchronous code
- Vitest: Mocking functions
- Vitest 4 migration guide
(最終確認: 2026年7月25日)
← 一覧に戻る