finding-seams について
finding-seams は、隠れた依存関係を持つコードのテストを支援するスキルです。範囲を絞った置換ポイントの導入方法を説明し、明示的な引数注入を優先して、モジュールモックは一時的な足場として扱います。
主な機能
- 01テストを妨げる隠れた依存関係の特定
- 02関数引数を置換ポイントとして利用
- 03TypeScript と JavaScript のシーム選択
- 04本番時のデフォルトと呼び出し箇所の維持
- 05不要になった依存モックの削除
活用シーン
- 01外部システムを直接呼び出すコードのテスト
- 02グローバルやシングルトンを読む関数の分離
- 03レガシーコードの振る舞い特性テストの準備
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ citypaul/ .dotfiles --skill finding-seamsインストール後、Claude Code で/finding-seamsを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
resources/creating-seams.mdoop-patterns.mdseam-types.md詳細
シームを見つける
シームを設けた後、既存の動作を記録するテストを書くには、characterisation-tests スキルを読み込みます。新しい動作をテスト駆動で実装するには、tdd スキルを読み込みます。一般的なテストパターンについては、testing スキルを読み込みます。テストを整えた後のリファクタリングには、refactoring スキルを読み込みます。モジュールの長期的な責務と呼び出し元に対する契約を決める際は、codebase-design を使用します。このスキルでは、既存の動作をテストできるようにするために必要な最小限の有効化ポイントを紹介します。
詳しいリソースは resources/ ディレクトリにあります。必要に応じて読み込んでください。
| リソース | 読み込むタイミング |
|---|---|
seam-types.md | TypeScript における各シームタイプの 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つも書かないうちに、次の手順を進めてください。
- 関数が利用しているすべての隠れた依存関係を洗い出します。関数内で生成する協力オブジェクト、
Date.now()/new Date()、process.env、シングルトンなどです。最も気になるものだけではなく、すべてです。 - それぞれを、本番環境で現在の動作を正確に再現するデフォルト値とともに引数リストへ移します。
newと環境変数の読み取りはデフォルト値の中に置くため、本番環境の動作は変わりません。これが済むと、テスト対象の関数は何も生成せず、グローバルも読み取りません。 - 引数としてフェイクを渡してテストを書きます。フェイクとは、テスト内で手書きし、完全に制御できる代用品です。
() => 1_700_000_000_000のようなリテラルや、シームの狭い型で指定されたメソッドだけを実装する小さなオブジェクトが該当します。シームを通じて本物の協力オブジェクトを渡してはいけません。本番のクライアント、ドライバー、サービスオブジェクトは、その実装がどれほど安価で、ローカルで、メモリ内のものであっても、またそのコメントに何と書かれていても、渡してはいけません。本物の協力オブジェクトを使ったテストは、そのオブジェクトの形状が変わると壊れます。これは、シームで断ち切ろうとしている結合そのものです。テストの実行にvi.mock()、vi.stubEnv()、またはフェイクタイマーが必要なら、まだ依存関係が隠れています。それらに頼るのではなく、手順2に戻ってください。 - 既存のテストにある、その依存関係のモジュールモックを削除します。シームがモックに取って代わるため、両方を残すと、古いテストでは依然としてモックに隠されたものを確認できません。
作業を引き渡すときは、導入したシームの型と、それを有効にする箇所(引数のデフォルト値、?? によるフォールバック、またはファクトリー呼び出しのファイルと行)を明記し、既存の呼び出し箇所は変更していないことを確認してください。
シームの見つけ方
テスト対象のコードで、次の点を確認してください。
- 関数パラメーター — 異なる実装を受け入れられるパラメーター
- デフォルトパラメーター値 —
(resolve = fetchFromApi)は、すでにシームになっています - モジュールのインポート — インポートされたものはモックできる可能性があります(ただし、パラメーター注入を優先してください)
- 設定 — 環境変数、設定ファイル、機能フラグ
- React の props とコンテキスト — コンポーネントは依存関係を props として受け取ります。テストではコンテキストプロバイダーを差し替えられます
- ハードコードされた
newや直接呼び出し — 直接的な依存関係がある場所はすべて、シームを設けられる可能性がありますが、まだ設けられていません
段階
推奨順から最終手段の順です。機能する最も明示的な方法から始めてください。
- 関数パラメーターによる注入 — 本番環境でのデフォルト値を設定し、依存関係を引数として渡します(明示的で、型安全で、フレームワーク不要)
- 高階関数 — ファクトリーから設定済みの関数を返します(関数型プログラミングの合成)
- 設定の注入 — グローバルから読み取る代わりに、設定や環境変数をパラメーターとして渡します
- モジュールモック — インポートを置き換えるための
vi.mock()(足場のみ — カバレッジが増えたら別の方法へ移行します) - サブクラス化とオーバーライド — レガシーなクラスベースのコードに限ります(
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つずつ切り離し、テストを通してから次に進んでください |