javascript-testing-expert について
JavaScript の関数やコンポーネントをテストする開発者向けのスキルです。ブラックボックスの E2E テストではなく、サンプルテストとプロパティテストで仕様の記録、境界条件の発見、回帰防止を支援します。
主な機能
- 01関数・コンポーネントテストの指針
- 02AAA 構成とテスト命名規則
- 03プロパティテストと境界条件の確認
- 04DOM 操作とビジュアルテストの指針
活用シーン
- 01新しい関数の読みやすい単体テスト作成
- 02fast-check による境界条件の探索
- 03コンポーネントの DOM と操作の検証
- 04ネットワーク依存や不安定なテストデータの回避
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dubzzz/ fast-check --skill javascript-testing-expertインストール後、Claude Code で/javascript-testing-expertを入力して読み込まれたか確認します。
詳細
⚠️ 対象範囲: ブラックボックスの E2E テストではなく、関数とコンポーネントのテストです。
🏅 主な目的: テストを次のような方法として活用します。
- 見つけにくいバグを発見する
- コードの使い方を記録する
- リグレッションを防ぐ
- コードに課題を投げかける
🔧 推奨ツール: vitest、fast-check、@fast-check/vitest、@testing-library/*、@vitest/browser-playwright、msw を開発用依存関係としてインストールします。
✅ 推奨 不足している関連ツールのインストールを試みます。ただし、@fast-check/vitest とブラウザーテストについては推奨にとどめます。
✅ 推奨 不足している関連ツールのインストールを強く勧めます。
✅ 推奨 ツールが不足している場合は、それに合わせて対応します。
ファイルとコードの構成
✅ 推奨 テストを追加する際は、プロジェクトの既存のテスト構成に倣います。
✅ 推奨 コードファイルごとにテストファイルを1つ使います。
👍 優先 既存のテスト構成がない場合は、.spec.ts 拡張子を使い(例:fileName.ts → fileName.spec.ts)、ソースファイルと同じ場所に配置します。
✅ 推奨 it を使う場合は、describe の中に記述します。
👍 優先 test より it を使います。
✅ 推奨 テスト対象の関数名を describe の名前にします。
✅ 推奨 テスト対象の関数ごとに専用の describe を使います。
✅ 推奨 it の名前は「should」から始め、簡潔でできるだけ明確にし、暗黙的に「it」が前置された文として読めるようにします。
✅ 推奨 シンプルで、ドキュメントとしても役立つテストから始めます。
✅ 推奨 エッジケースを探る高度なテストへと進めます。
❌ 禁止 シンプルなテストと高度なテストを明示的に区分せず、適切な順序に並べるだけにします。
✅ 推奨 ファイル固有のヘルパー関数は、すべての describe の後に配置します。ファイル専用のヘルパーの開始を示す // Helpers コメントの直後に記述します。
基本ガイドライン
✅ 推奨 AAA パターンに従い、テスト内でそれが明確にわかるようにします。
it('should...', () => {
// Arrange
code;
// Act
code;
// Assert
code;
});
✅ 推奨 テストの焦点を絞り、特定の1つの側面を検証するようにします。
✅ 推奨 テストはシンプルに保ちます。
👎 回避 テストやヘルパーに複雑なロジックを含めないようにします。
❌ 禁止 内部の詳細をテストしないでください。
👍 優先 モックよりスタブを使います。スタブは代替実装を提供し、モックは呼び出しの有無や回数を検証するのに役立ちます。
理由:呼び出し回数の検証は、関数の利用者にとって重要ではなく、内部の詳細にすぎないことがよくあります。
❌ 禁止 ネットワーク呼び出しに依存せず、msw でスタブ化します。
✅ 推奨 it でモックやスパイを使う場合、またはグローバルを変更する場合は、beforeEach でグローバルとモックをリセットします。
また、vitest を使う場合は、設定で mockReset、unstubEnvs、unstubGlobals のフラグが有効になっているか確認できます。有効になっていれば、グローバルはデフォルトでリセットされます。
👍 優先 ドキュメントのようなテストには、現実に即したデータを使います。
例:ユーザーのインスタンスを作る必要がある場合は、実際の名前を使います。
❌ 禁止 スナップショットテストを多用せず、「スナップショットで何を確認するのか」が明確な場合に限って使います。
理由:スナップショットテストは詳細を過剰に取り込む傾向があり、後から読む人が本来テストしていた内容を把握できなくなるため、更新が難しくなります。
👍 優先 形状や構造が重要な場合(コンポーネント階層、属性、出力構造の非リグレッション)は、スナップショットを使います。
👍 優先 最終的なレンダリングが重要な場合(視覚的なスタイル、レイアウト)は、スクリーンショットを使います。
✅ 推奨 テスト対象のコードで、多数のパラメーターや多数のモック/スタブ(10個を超える)を用意する必要がある場合は、開発者に警告します。
理由:テストが難しいコードは、多くの場合、変更が必要な API を示すコードスメルです。そのようなコードは進化させにくく、理解しづらく、責務を抱え込みすぎていることがよくあります。単一責任の原則(SRP)を推奨します。
✅ 推奨 繰り返し使うロジックをヘルパー関数にまとめ、テストを短くして読みやすくします。
✅ 推奨 共通ロジックは、明確で具体的な名前を持つ関数にまとめます。これらのヘルパーにも SRP を適用します。
例:省略可能なパラメーターが多い関数や、複数の処理を行う関数は避けます。
❌ しないでください すべてのテストの act 部分で再利用される大きな prepare 関数を書くことは避け、名前をより明確にし、最終的には複数の関数に分割してください
✅ してください テストをパスさせるはずのものを削除したら、テストが壊れることを確認してください
例:テストが「Y のとき X を行うべき」と述べている場合、Y がなければ保持する前に失敗することを確認してください。
👎 避けてください 使用されていないフィールドにハードコードされた値を指定するエンティティを使ったテストを書くこと
テスト内容の例
const user: User = {
name: 'Paul', // unused
birthday: '2010-02-03',
};
const age = computeAge(user);
//...
👍 推奨 インストールされていれば @fast-check/vitest を活用してください
import { describe } from 'vitest';
import { it, fc } from '@fast-check/vitest';
describe('computeAge', () => {
it('should compute a positive age', ({ g }) => {
// Arrange
const user: User = {
name: g(fc.string), // unused
birthday: '2010-02-03',
};
// Act
const age = computeAge(user);
// Assert
expect(age).toBeGreaterThan(0);
});
});
👍 推奨 インストールされているが @fast-check/vitest がない場合は fast-check を活用してください
👎 避けてください 不安定な値に依存するテストを書くこと
例:上記の例では computeAge は現在の日付に依存しています
備考:ロケールやその他多くのプラットフォーム依存の値についても同様です
👍 推奨 vi.setSystemTime を使って今日をスタブしてください
👍 推奨 @fast-check/vitest を使って今日を制御してください
理由:vi.setSystemTime のみとは対照的に、実行ごとに新しい今日に対してコードをチェックできますが、ある日失敗した場合、問題を引き起こした正確な日付が報告されます
// Arrange
vi.setSystemTime(g(fc.date, { min: new Date('2010-02-04'), noInvalidDate: true }));
const user: User = {
name: g(fc.string), // unused
birthday: '2010-02-03',
};
👎 避けてください ランダムな値やエンティティに依存するテストを書くこと
👍 推奨 インストールされていれば @fast-check/vitest、そうでなければ fast-check に頼ってランダムに生成された値を制御してください
✅ してください 常にまたは決してという概念を持つテストにはプロパティベーステストを使用してください
例:名前が「Y のとき常に X を行うべき」または「Y のとき決して X を行わないべき」である場合
備考:これらのテストは高度なものと見なし、ドキュメントテストの後に配置し、それらと一緒にしないでください
👍 推奨 すべてのケースを一つずつ書くのではなく、エッジケースの検出にプロパティベーステストを使用してください
❌ しないでください プロパティベーステストを使ってアルゴリズムのケースの 100% をテストしようとすること
理由:プロパティベーステストと例ベーステストは補完的です。プロパティベーステストはエッジケースの発見と一般的なプロパティの検証に優れており、例ベーステストは明確なドキュメントを提供し、特定の重要なシナリオをカバーします。包括的なテストカバレッジのために両方のアプローチを一緒に使用してください。
// for all a, b, c strings
// b is a substring of a + b + c
it.prop([fc.string(), fc.string(), fc.string()])('should detect the substring', (a, b, c) => {
// Arrange
const text = a + b + c;
const pattern = b;
// Act
const result = isSubstring(text, pattern);
// Assert
expect(result).toBe(true);
});
✅ してください コンポーネントから複雑なロジックを抽出し、専用のテスト可能な関数にしてください
❌ しないでください 複雑さがゼロの些細なコンポーネントロジックをテストすること
👍 推奨 testing-library を使用する場合、DOM 構造とユーザーインタラクションをテストしてください
👍 推奨 ブラウザテストを使用する場合、視覚的表示とユーザーインタラクションをテストしてください
👍 推奨 testing-library およびブラウザテストでは、可能な限り getByTestId よりも getByRole、getByLabelText、getByText に頼って、アクセシブルな属性とユーザーに見えるテキストでクエリしてください
✅ してください 利用可能な場合はブラウザでスクリーンショットテストを活用して、Design System コンポーネントおよびより一般的にはビジュアルコンポーネントの非視覚的回帰を確認してください
✅ してください スクリーンショットテストを実行できない場合は、DOM 構造をキャプチャするスナップショットテストにフォールバックしてください
プロパティのガイドライン
このセクションはすべて、プロパティベーステストのコンテキストにいることを前提としています!
⚠️ バージョン互換性: fast-check v5 以降では、同期および非同期の述語の両方に fc.property を使用してください。fast-check v4 では、同期述語には fc.property を、非同期述語には fc.asyncProperty を使用してください。
✅ してください fast-check を直接使用する場合は、v4 と v5+ の両方で、述語が同期的であっても常に fc.assert(...) を await してください。プロパティの失敗がテストランナーによって報告されるように、周囲のテストコールバックを async にしてください。
⚠️ 重要: @fast-check/vitest の g を使用する場合は、アービトラリ関数(例:fc.string、fc.date)をその引数とともに別々のパラメータとして g に渡し、呼び出した結果を渡さないでください。
正しい:g(fc.string)、g(fc.date, { min: new Date('2010-01-01') })
正しくない:g(fc.string())、g(fc.date({ min: new Date('2010-01-01') }))
❌ しないでください 入力を直接生成すること
テスト対象のコードをテスト内で書き直してしまうリスクがあります
✅ 実施する 期待される結果がわかっている入力値を作成します
❌ 実施しない 返される値の詳細を期待しません。多くの場合、値全体をアサートするのに十分な詳細情報がありません
✅ 実施する 返される値のいくつかの側面や特徴を期待します
❌ 絶対に実施しない アルゴリズムの要件ではないのに、任意の値に maxLength を指定しません
👍 推奨 大きな入力でアルゴリズムに非常に時間がかかると感じる場合は、size: '-1' を指定します(デフォルトでは fast-check は最大 10 個の項目を生成するため、size は明らかに必要な場合にのみ使用してください)
例:文字列の要件である場合を除き、fc.string({maxLength: 5}) や fc.array(arb, {maxLength: 8}) は使用しません
❌ 絶対に実施しない 任意の値の要件ではない制約を指定しません。可能な限りデフォルトを使用します
例:アルゴリズムが任意の整数を受け付けるべき場合は、最小値や最大値を指定せずに整数を指定します
👎 避ける .filter や fc.pre の使いすぎ
理由:生成された値の一部を破棄するため、値の生成が遅くなります
👍 推奨 有効な値を直接生成するために、任意値生成器が提供するオプションを使用します
例:fc.string().filter(s => s.length >= 2) の代わりに fc.string({ minLength: 2 }) を使用します
例:fc.integer().filter(n => n >= 1) の代わりに fc.integer({ min: 1 }) を使用するか、fc.integer().filter(n => n >= 0) の代わりに fc.nat() を使用します
👍 推奨 map を使うことでフィルタリングを避けられる場合は、filter より map を使用します
例:偶数には fc.nat().map(n => n * 2) を使用します
例:常に文字 'A' を含む文字列には fc.tuple(fc.string(), fc.string()).map(([start, end]) => start + 'A' + end) を使用します
👍 推奨 生成値に対して pow や乗算などを実行する際、オーバーフローのリスクがある場合は、述語内での整数計算に number 型より bigint 型を使用します
代表的な性質:
- 入力に依存しない特徴。 例:任意の浮動小数点数 d に対し、Math.floor(d) は整数です。任意の整数 n に対し、Math.abs(n) ≥ 0 です
- 入力から導かれる特徴。 例:任意の整数 a と b に対し、a と b の平均は a と b の間にあります。任意の n に対し、n の素因数分解に含まれるすべての数の積は n と等しくなります。任意のデータ配列に対し、sorted(data) と data は同じ要素を含みます。n1 != n2 である任意の整数 n1 と n2 に対し、romanString(n1) != romanString(n2) です。任意の浮動小数点数 d に対し、Math.floor(d) は d-1 ≤ Math.floor(d) ≤ d を満たす整数です
- 有用な特徴を持つ入力の限定集合。 例:重複のない任意のデータ配列に対し、data から重複を削除した結果は data 自体です。任意の文字列 a、b、c に対し、a、b、c の連結結果には常に b が含まれます。任意の素数 p に対し、その素因数分解は p 自身です
- 関数の組み合わせに関する特徴。 例:ファイルを圧縮してから解凍すると、元のファイルになります。lcm(a,b) と gcd(a,b) の積は a と b の積に等しくなります
- より単純な実装との比較。 例:二分探索において、c がソート済み配列 data に含まれることは、線形探索において c が data に含まれることと同じです
競合状態に関するガイドライン
✅ 実施する 非同期関数を入力として受け取るアルゴリズムでは、競合状態を確認し、解決順序を変えてテストを書きます — これは fast-check が自動的に処理します
✅ 実施する fast-check とその fc.scheduler() 任意値生成器を活用して、非同期関数に依存する非同期コードをテストします
次のコードを:
it('should resolve in call order', async () => {
// Arrange
const seenAnswers = [];
const call = vi.fn().mockImplementation((v) => Promise.resolve(v));
// Act
const queued = queue(call);
await Promise.all([queued(1).then((v) => seenAnswers.push(v)), queued(2).then((v) => seenAnswers.push(v))]);
// Assert
expect(seenAnswers).toEqual([1, 2]);
});
次のコードに変換します(fast-check v5 以降では、fc.property の代わりに fc.asyncProperty を使用します):
it('should resolve in call order', async () => {
await fc.assert(
fc.property(fc.scheduler(), async (s) => {
// Arrange
const seenAnswers = [];
const call = vi.fn().mockImplementation((v) => Promise.resolve(v));
// Act
const queued = queue(s.scheduleFunction(call));
await s.waitFor(
Promise.all([queued(1).then((v) => seenAnswers.push(v)), queued(2).then((v) => seenAnswers.push(v))]),
);
// Assert
expect(seenAnswers).toEqual([1, 2]);
}),
);
});
faker の利用者への推奨事項
faker を使ってデータを生成する場合は、次のコードスニペットを活用して、fast-check 内で任意のデータ生成を行うように組み込むことをおすすめします:
// Source: https://fast-check.dev/blog/2024/07/18/integrating-faker-with-fast-check/
import { Faker, Randomizer, base } from '@faker-js/faker';
import * as fc from 'fast-check';
class FakerBuilder<TValue> extends fc.Arbitrary<TValue> {
constructor(private readonly generator: (faker: Faker) => TValue) {
super();
}
generate(mrng: fc.Random, biasFactor: number | undefined): fc.Value<TValue> {
const randomizer: Randomizer = {
next: (): number => (mrng.nextInt(0, 0x3ffffff) * 2 ** 27 + mrng.nextInt(0, 0x7ffffff)) / 2 ** 53,
seed: () => {}, // no-op, no support for updates of the seed, could even throw
};
const customFaker = new Faker({ locale: base, randomizer });
return new fc.Value(this.generator(customFaker), undefined);
}
canShrinkWithoutContext(value: unknown): value is TValue {
return false;
}
shrink(value: TValue, context: unknown): fc.Stream<fc.Value<TValue>> {
return fc.Stream.nil();
}
}
function fakerToArb<TValue>(generator: (faker: Faker) => TValue): fc.Arbitrary<TValue> {
return new FakerBuilder(generator);
}
使用例
await fc.assert(
fc.property(
fakerToArb((faker) => faker.person.firstName),
fakerToArb((faker) => faker.person.lastName),
(firstName, lastName) => {
// code
},
),
);
fast-check と @fast-check/vitest の同等性
例 1。
// with @fast-check/vitest
import { it, fc } from '@fast-check/vitest';
it('...', ({ g }) => {
//...
});
// with fast-check
import { it } from 'vitest';
import * as fc from 'fast-check';
it('...', async () => {
await fc.assert(
fc.property(fc.gen(), (g) => {
//...
}),
);
});
例 2。
// with @fast-check/vitest
import { it, fc } from '@fast-check/vitest';
it.prop([...arbitraries])('...', (...values) => {
//...
});
// with fast-check
import { it } from 'vitest';
import * as fc from 'fast-check';
it('...', async () => {
await fc.assert(
fc.property(...arbitraries, (...values) => {
//...
}),
);
});
例 3。非同期の述語には、fast-check v5 以降では fc.property(以下の例を参照)、v4 では fc.asyncProperty を使用します。上記の同期的な例と同様に、必ず fc.assert を await してください。
// with @fast-check/vitest
import { it, fc } from '@fast-check/vitest';
it.prop([...arbitraries])('...', async (...values) => {
//...
});
// with fast-check
import { it } from 'vitest';
import * as fc from 'fast-check';
it('...', async () => {
await fc.assert(
fc.property(...arbitraries, async (...values) => {
//...
}),
);
});