mcpField
J
javascript-testing-expert

javascript-testing-expert

github.com/dubzzzテスト・品質5.2k257 回インストール今日更新
リポジトリ

javascript-testing-expert について

JavaScript の関数やコンポーネントをテストする開発者向けのスキルです。ブラックボックスの E2E テストではなく、サンプルテストとプロパティテストで仕様の記録、境界条件の発見、回帰防止を支援します。

主な機能

  1. 01関数・コンポーネントテストの指針
  2. 02AAA 構成とテスト命名規則
  3. 03プロパティテストと境界条件の確認
  4. 04DOM 操作とビジュアルテストの指針

活用シーン

  1. 01新しい関数の読みやすい単体テスト作成
  2. 02fast-check による境界条件の探索
  3. 03コンポーネントの DOM と操作の検証
  4. 04ネットワーク依存や不安定なテストデータの回避

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/dubzzz/fast-check --skill javascript-testing-expert

インストール後、Claude Code で/javascript-testing-expertを入力して読み込まれたか確認します。

詳細

⚠️ 対象範囲: ブラックボックスの E2E テストではなく、関数とコンポーネントのテストです。

🏅 主な目的: テストを次のような方法として活用します。

  1. 見つけにくいバグを発見する
  2. コードの使い方を記録する
  3. リグレッションを防ぐ
  4. コードに課題を投げかける

🔧 推奨ツール: 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 型を使用します

代表的な性質:

  1. 入力に依存しない特徴。 例:任意の浮動小数点数 d に対し、Math.floor(d) は整数です。任意の整数 n に対し、Math.abs(n) ≥ 0 です
  2. 入力から導かれる特徴。 例:任意の整数 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 を満たす整数です
  3. 有用な特徴を持つ入力の限定集合。 例:重複のない任意のデータ配列に対し、data から重複を削除した結果は data 自体です。任意の文字列 a、b、c に対し、a、b、c の連結結果には常に b が含まれます。任意の素数 p に対し、その素因数分解は p 自身です
  4. 関数の組み合わせに関する特徴。 例:ファイルを圧縮してから解凍すると、元のファイルになります。lcm(a,b) と gcd(a,b) の積は a と b の積に等しくなります
  5. より単純な実装との比較。 例:二分探索において、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) => {
      //...
    }),
  );
});

コメント