mcpField
F
finding-seams

finding-seams

github.com/citypaulテスト・品質760178 回インストール4 週間前に更新
リポジトリ

finding-seams について

finding-seams は、隠れた依存関係を持つコードのテストを支援するスキルです。範囲を絞った置換ポイントの導入方法を説明し、明示的な引数注入を優先して、モジュールモックは一時的な足場として扱います。

主な機能

  1. 01テストを妨げる隠れた依存関係の特定
  2. 02関数引数を置換ポイントとして利用
  3. 03TypeScript と JavaScript のシーム選択
  4. 04本番時のデフォルトと呼び出し箇所の維持
  5. 05不要になった依存モックの削除

活用シーン

  1. 01外部システムを直接呼び出すコードのテスト
  2. 02グローバルやシングルトンを読む関数の分離
  3. 03レガシーコードの振る舞い特性テストの準備

インストール

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

$npx skills add https://github.com/citypaul/.dotfiles --skill finding-seams

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

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

resources/
creating-seams.md
oop-patterns.md
seam-types.md

詳細

シームを見つける

シームを設けた後、既存の動作を記録するテストを書くには、characterisation-tests スキルを読み込みます。新しい動作をテスト駆動で実装するには、tdd スキルを読み込みます。一般的なテストパターンについては、testing スキルを読み込みます。テストを整えた後のリファクタリングには、refactoring スキルを読み込みます。モジュールの長期的な責務と呼び出し元に対する契約を決める際は、codebase-design を使用します。このスキルでは、既存の動作をテストできるようにするために必要な最小限の有効化ポイントを紹介します。

詳しいリソースは resources/ ディレクトリにあります。必要に応じて読み込んでください。

リソース読み込むタイミング
seam-types.mdTypeScript における各シームタイプの FP 優先の詳しい例が必要な場合
creating-seams.mdシームがない場所に導入する必要があり、導入前後の例が必要な場合
oop-patterns.mdレガシーなクラスベースのコードに遭遇した場合――オブジェクトシーム、サブクラス化とオーバーライド、コンストラクターインジェクション

中心となる概念

シームとは、その場所を編集せずにプログラムの動作を変更できる箇所です。

すべてのシームには有効化ポイントがあります。これは、どの動作を有効にするかを選択する場所です。シーム部分のソースコードは本番環境とテストで同一のままで、有効化ポイントだけが異なります。

シームは、自動的に公開モジュールインターフェース、ポート、または恒久的な抽象化になるわけではありません。まずは安全に置き換えられる最小限のポイントを導入します。動作の特性を明らかにした後、そのポイントを永続的な契約に含めるか、非公開の足場として残すかを codebase-design に判断させます。

-- Michael Feathers, Working Effectively with Legacy Code (2004)

ヘキサゴナルアーキテクチャとの関係: ポートは設計段階で組み込まれるシームです。ポートが契約(シーム)を定義し、コンポジションルートが接続するアダプター(有効化ポイント)を選択します。コードですでにヘキサゴナルアーキテクチャを採用しているなら、シームはあらゆる場所にあります。このスキルは、シームがないコードを対象としています。hexagonal-architecture スキルを参照してください。

使用する場面

  • 関数が外部システムに直接アクセスするため、テストハーネスから呼び出せない
  • 関数が依存関係をパラメーターとして受け取らず、ハードコードしている
  • グローバルまたは静的な依存関係のために分離できない
  • シングルトンへのアクセスパターンによって、コードが共有された可変状態に結合している
  • React コンポーネントが props/context 経由でデータを受け取るのではなく、内部でデータを取得している

クイックリファレンス:TypeScript/JS のシームタイプ

シームタイプ仕組み有効化ポイント優先する場面
関数パラメーター依存関係を引数として渡す引数リストデフォルトの選択肢。 関数型のコード、純粋関数、明示的な契約
設定設定値や環境変数を引数として受け取るそれらを受け取る関数の引数リストインフラストラクチャーレベルの懸念事項。テスト対象の関数内に残された process.env の読み取りは、設定シームではなく隠れた依存関係です。外に移し、デフォルトの処理で読み取るようにします
モジュールvi.mock() / jest.mock() でインポートを置き換えるテストファイルのモック設定最後の手段。 手早く足場を作る場合に限ります――型安全性を迂回し、暗黙的で、後始末が必要です。シグネチャを変更できるなら新たに作成しないでください。また、隣接するテストからコピーしないでください。既存のモジュールモックは標準パターンではなく、移行して廃止するための足場です
オブジェクトサブクラス化してオーバーライドする、またはコンストラクター経由で DI するオブジェクトを作成する場所レガシーなクラスベースのコード(resources/oop-patterns.md を参照)

テスト対象に既存コードをする

テストを1つも書かないうちに、次の手順を進めてください。

  1. 関数が利用しているすべての隠れた依存関係を洗い出します。関数内で生成する協力オブジェクト、Date.now()/new Date()、process.env、シングルトンなどです。最も気になるものだけではなく、すべてです。
  2. それぞれを、本番環境で現在の動作を正確に再現するデフォルト値とともに引数リストへ移します。new と環境変数の読み取りはデフォルト値の中に置くため、本番環境の動作は変わりません。これが済むと、テスト対象の関数は何も生成せず、グローバルも読み取りません。
  3. 引数としてフェイクを渡してテストを書きます。フェイクとは、テスト内で手書きし、完全に制御できる代用品です。() => 1_700_000_000_000 のようなリテラルや、シームの狭い型で指定されたメソッドだけを実装する小さなオブジェクトが該当します。シームを通じて本物の協力オブジェクトを渡してはいけません。本番のクライアント、ドライバー、サービスオブジェクトは、その実装がどれほど安価で、ローカルで、メモリ内のものであっても、またそのコメントに何と書かれていても、渡してはいけません。本物の協力オブジェクトを使ったテストは、そのオブジェクトの形状が変わると壊れます。これは、シームで断ち切ろうとしている結合そのものです。テストの実行に vi.mock()、vi.stubEnv()、またはフェイクタイマーが必要なら、まだ依存関係が隠れています。それらに頼るのではなく、手順2に戻ってください。
  4. 既存のテストにある、その依存関係のモジュールモックを削除します。シームがモックに取って代わるため、両方を残すと、古いテストでは依然としてモックに隠されたものを確認できません。

作業を引き渡すときは、導入したシームの型と、それを有効にする箇所(引数のデフォルト値、?? によるフォールバック、またはファクトリー呼び出しのファイルと行)を明記し、既存の呼び出し箇所は変更していないことを確認してください。

シームの見つけ方

テスト対象のコードで、次の点を確認してください。

  1. 関数パラメーター — 異なる実装を受け入れられるパラメーター
  2. デフォルトパラメーター値 — (resolve = fetchFromApi) は、すでにシームになっています
  3. モジュールのインポート — インポートされたものはモックできる可能性があります(ただし、パラメーター注入を優先してください)
  4. 設定 — 環境変数、設定ファイル、機能フラグ
  5. React の props とコンテキスト — コンポーネントは依存関係を props として受け取ります。テストではコンテキストプロバイダーを差し替えられます
  6. ハードコードされた new や直接呼び出し — 直接的な依存関係がある場所はすべて、シームを設けられる可能性がありますが、まだ設けられていません

段階

推奨順から最終手段の順です。機能する最も明示的な方法から始めてください。

  1. 関数パラメーターによる注入 — 本番環境でのデフォルト値を設定し、依存関係を引数として渡します(明示的で、型安全で、フレームワーク不要)
  2. 高階関数 — ファクトリーから設定済みの関数を返します(関数型プログラミングの合成)
  3. 設定の注入 — グローバルから読み取る代わりに、設定や環境変数をパラメーターとして渡します
  4. モジュールモック — インポートを置き換えるための vi.mock()(足場のみ — カバレッジが増えたら別の方法へ移行します)
  5. サブクラス化とオーバーライド — レガシーなクラスベースのコードに限ります(resources/oop-patterns.md を参照)

手順1〜3は、長期的に維持する契約として妥当な場合に限り、永続的な設計の候補となります。そうでない場合は、非公開にするか、特性を明らかにした後で削除してください。手順4〜5は通常、一時的な足場です。

簡単な例

scheduleDelivery の特性を明らかにするには、まず隠れた依存関係に対するシームが必要です。

// BEFORE -- no seam, can't test without hitting real API
const scheduleDelivery = (delivery: Delivery): DeliveryPlan => {
  const transitDays = fetchTransitDays(delivery.region);
  return { ...delivery, totalDays: delivery.preparationDays + transitDays };
};

// AFTER -- function parameter seam with production default
type TransitDaysResolver = (region: string) => number;

const scheduleDelivery = (
  delivery: Delivery,
  resolveTransitDays: TransitDaysResolver = fetchTransitDays,
): DeliveryPlan => {
  const transitDays = resolveTransitDays(delivery.region);
  return { ...delivery, totalDays: delivery.preparationDays + transitDays };
};

// Test -- swap in a fake at the enabling point (the argument list)
const result = scheduleDelivery(testDelivery, () => 2);

デフォルト値が適用されるため、すべての呼び出し箇所で本番コードに変更はありません。テストではフェイクを渡します。シームはパラメーターであり、それを有効にする箇所は引数リストです。

コードの臭い → テクニック

コード内で見られるものテクニック例
関数内の new Foo()関数をパラメーター化する依存関係をデフォルト値付きのパラメーターとして渡す
process.env.X を直接読み取るグローバル呼び出しをラップする(getEnv = () => process.env.X)
import { thing } from './heavy-lib' を直接使用する型を抽出してパラメーター化する狭い範囲の type を定義し、パラメーターとして渡す
1つの関数内に複数のハードコードされた依存関係がある高階関数ファクトリーcreateFn(deps) => (args) => result
SingletonClass.getInstance()グローバル呼び出しをラップする(getSingleton = () => SingletonClass.getInstance())
Date.now() / Math.random()グローバル呼び出しをラップする(now = Date.now) をパラメーターとして渡す
クラスが自身の協力オブジェクトを生成するコンストラクターをパラメーター化する(OOP)コンストラクター経由で受け取る。oop-patterns.md を参照
まだ関数シグネチャを変更できないモジュール間接参照(足場)薄いラッパーモジュールと vi.mock() を使い、後で移行する

よくある間違い

間違い修正方法
vi.mock() を恒久的なアーキテクチャとして使うモジュールモックは型安全性を回避し、暗黙的な結合を生みます。テストができたら、できるだけ早くパラメーター注入に移行してください。
クラスベースのパターン(サブクラス、DI コンテナー)を最初に使うTypeScript の関数型プログラミングでは、関数パラメーターが自然な接続点になります。クラスや DI コンテナーはほとんど必要ありません。
実際の接続点を見つけずにすべてをモックする接続点の境界だけをモックし、実際のロジックをテストしてください
実装の詳細が漏れる接続点を作る接続点のインターフェースは「どのように」ではなく「何を」を記述する必要があります
有効化ポイントを忘れるすべての接続点には動作を選択する場所が必要です。有効化ポイントがなければ、それは接続点ではありません
一度に多くの依存関係を切り離す依存関係を1つずつ切り離し、テストを通してから次に進んでください

コメント