Communityライティング&編集github.com

meganemura/nukadoko

Gherkin用の型付きステップ契約。シナリオが実行される前に間違いがわかる。語彙はエージェントにとって機械可読であり、すべての実行はエージェントが報告するのではなくツールが測定したレシートを残す。

nukadoko とは?

このスキルは、既存のテストスイートを段階的に「nukadoko」へ移行するための手法を提供します。nukadokoは、Gherkinシナリオのステップに型安全な契約(入力スキーマ、戻り値、依存関係、変更の有無)を追加できるフレームワークです。最も重要な原則は「2つの変更を同時に行わない」こと。これにより、障害の原因を正確に特定できます。cucumber-jsからの移行では、まずインポートを変更して互換モードで実行し(Stage 1)、その後ステップごとに型付きステップへ昇格させます(Stage 2)。すでに型付きDSLを持っている場合は、Gherkinパターンは後から追加可能です。nukadokoはClaude CodeやCursorなどのAIエージェント向けに設計されており、ステップの語彙が機械可読であるため、エージェントは誤ったシナリオを実行前に検出できます。また、各実行のレシートにはツールが測定した実際の情報が記録され、エージェントの自己申告に頼りません。

対応~Claude Code~Codex CLI~Cursor
npx skills add meganemura/nukadoko

Installed? Explore more ライティング&編集 skills: steipete/notion, langchain-ai/langchain, bytedance/podcast-generation · View all 6 →

お気に入りのAIに質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

ドキュメント

nukadokoへの移行

これは何のためか

既存のスイートを、一度にすべて書き換えるのではなく、少しずつnukadokoに移行するためのものです。 開始地点は以下のいずれかです。

  • cucumber-jsスイート — フィーチャーファイルとグルー(通常はPlaywrightを駆動)
  • すでに型付きステップとして整形されたスイート — cucumberではなく独自のDSLだが、nukadokoのdefineStepに十分近いため、移行は主に翻訳作業となる

どちらの場合も、移行は段階的に行われ、決して一度にすべてを変更しません。このスキルではその理由を説明し、次に各開始地点について順に説明します。

以下のすべては、プロジェクトがすでに初期化されていることを前提としています(nukadoko.config.tsが存在しない場合は、最初にnuka initを実行してください)。

2つの段階、決して同時に行わないこと

一度に一つのことだけを変更し、次の変更はその後に行います。ステップが2つの変更を同時に行った後に壊れた場合、どちらの変更が原因かを特定できません。原因が単一の失敗に絡み合っているからです。作業を段階に分割することで、すべての失敗を正確に一つの変更に追跡可能に保ちます。これがすべての要点であり、段階自体はそれに次ぐものです。

以下の段階が自分の開始地点に合わない場合は、ここから自分自身の2つの段階を導き出してください。重要なのはトレーサビリティであり、これらの特定の段階名ではありません。

以下に2つの適用例を示します。cucumber-jsスイートは「インポートの切り替え」と「型付けの追加」に分割されます(下記)。すでに型付きステップとして整形されたスイートは別の方法で分割され、後述します。

cucumber-jsスイートからの移行

これは、上記の2つの段階の名前の由来となったケースです。

ステージ1 — 実行できるようにする

各グルーファイルが使用するインポートを1つ変更します。

// 変更前: import { Given, When, Then } from "@cucumber/cucumber";
import { Given, When, Then } from "nukadoko/compat";

ディスカバリが読み取るものを決めるのは拡張子であり、スイートの過去の履歴ではありません。.ts、.mts、.js、.mjsのグルーはすべて同じ方法で読み取られるため、これまでプレーンJavaScriptだったスイートでも、この段階を開始するためにTypeScriptに書き換える必要はありません。.cjsファイルは唯一の例外です。nukadokoはESMのみであるため、ディスカバリは.cjsをインポートしません。代わりにnuka checkがそのファイルを名前付きで報告します(step-file-unsupported-extension)。そのため、不明なundefined-stepとして消失することはありません。ファイルを.jsにリネームしても、ファイルのコードがすでにESモジュール構文である場合にのみ役立ちます。まだrequire(...)を呼び出している場合、同じESMのみの理由で同じインポートで失敗し、nuka checkはstep-file-import-failedとしてそれを報告します。

その後、nuka check <feature>とnuka run <feature>を実行し、その出力を読みます。指摘された問題を修正し、再度実行します。既存のスイートがグリーンになるまで繰り返します — これがステージ1の完了条件であり、それ以上ではありません。その条件はnuka checkとnuka runがグリーンになることであり、tscの型チェックがクリーンになることではありません。この2つはどちらの方向でも一致しない可能性があります。

nuka stepsとnuka describeはステージ1全体で使用可能です。一部のグルーファイルのインポートがまだ失敗していても、両方のコマンドはステップファイルを1つずつ読み取るため、インポートに失敗したファイルは(--jsonの場合はimport_failures、それ以外はstderrに)名前が報告され、語彙全体が空になることはありません。すべてのファイルがクリーンにインポートされるのを待たずに、作業を進めながら語彙を読み取ってください。nuka run <feature>とnuka do <step>は、意図的に、読み取り不可能なグルーファイルがある場合はそのまま拒否します。これは実行しようとしているからです。

開始する前に、compatがサポートしていないもののリストを探しに行かないでください。動作しないものは、インポート時または最初のnuka runで大きな音を立てて失敗します。失敗したものが何を壊したかを名前で示し、それがあなたが対処すべきことです。ここにリストを書いても、compatのカバレッジが拡大するたびに古くなります。CLI自身の出力は決して古くなりません。

ステージ2 — 契約を与える

スイートが実行されるようになったら、いくつかのステップに型を付ける価値があります。型を付ける際に変更されるものは次のとおりです。

compatステップ型付きステップ
入力パターンキャプチャのみ、未チェックargsスキーマで検証され、各フィールドに.describe()が付与される
出力破棄される — レシートのresultはnullreturnsスキーマで検証され、レシートに保存される
依存関係Worldへの副作用、関数シグネチャに非表示fromで宣言され、インポートで名前が付けられ、nuka checkで何よりも先にチェックされ、レシートにusedとして記録される
キーワード装飾的 — Thenにバインドされたステップでも変更可能mutatesはnukadokoが信頼する宣言。mutates: trueを宣言すると、読み取り専用環境はそのステップの実行を拒否し、nuka checkはThenにバインドされている場合に警告する。実際に実行された内容はレシートのobservedカウントに記録される
単独実行不可能(シナリオ外ではWorldは空)nuka do <ステップ>で直接実行可能。レシートはstdoutに出力される。fromキーは他のキーと同様に--argsから、または以前の実行結果から取得する場合は--use <レシートID>から取得される。run内部でのresultOfフィクスチャ呼び出しは、シナリオがないためチェーンをたどるものがなく、何も見つからない

最後の行は、上の「依存関係」の行とは別の事実であり、その結果ではありません。compatステップが単独実行できないのは、そのステップが必要とするものがまだ何も設定されていないWorld上に存在し、Worldがそのシグネチャの一部ではないため、何を最初にセットアップすべきかを検査する手段がないからです。型付きステップの依存関係はfromエントリとして名前が付けられ、インポートで可視化されるため、nuka do呼び出しで手動で供給できます。通常のキーは--args、以前の実行結果から取得する場合は--use <レシートID>です。上流のステップの名前自体はコマンドラインに現れる必要はありません。引用されたレシートがすでにそれを保持しているからです。すべてのキーがそのように到着するステップは、--argsをまったく必要としません。--useだけでも完全な呼び出しです。シナリオ外でまだ何も見つからないのは、run内部からresultOfフィクスチャを介して読み取られる依存関係です。その呼び出しは、--useの有無にかかわらず、シナリオがチェーンを構築しなかったため、たどるチェーンがありません。

最も頻繁に実行するステップから、一度に1つずつ昇格させます。スイート全体を一度に処理するのではありません。特定のグルーの書き換え方法は、その時点での判断に委ねられます。nukadokoは一つのレシピを規定せず、このスキルも同様です。

型付きステップのrunを起草する際、完全に分割構造化される前に、移行前のrun(ctx, args)シェイプに陥ることがあります。nuka steps --jsonは、その草稿が触れるフィクスチャをneeds_inferredとしてすでに名前付けしています。ctxのメンバーアクセスから読み取られた語彙上の推測であり、needs: nullと、実際のコントラクトがまだ読み取れない理由を説明するneeds_errorとともに表示されます。これにより、分割構造化されたシグネチャに何を追加すべきかを知るために本体全体を手動で再読する手間が省けますが、これは推測であり、コントラクトではありません。エイリアス(const c = ctx)を見逃し、needs_browserを決して名前付けしません。したがって、完了したと見なさずに分割構造化を完了してください。

昇格したステップの例

1つの例を示します。変更前のcompatグルー:

const { Given } = require("@cucumber/cucumber");

Given("a project {string} exists", async function (name) {
  const res = await this.request.post("/projects", { data: { name } });
  this.projectId = (await res.json()).id;
});

変更後、型付きステップ(features/steps/create-project.ts):

import { defineStep } from "nukadoko";
import { z } from "zod";

export default defineStep({
  pattern: "a project {name:string} exists",
  description: "プロジェクトを作成し、そのIDを返す",
  args: z.object({
    name: z.string().describe("プロジェクトの表示名"),
  }),
  returns: z.object({
    id: z.string().describe("作成されたプロジェクトのID。後続のステップで参照するため"),
  }),
  mutates: true,
  async run({ request }, args) {
    const res = await request.post("/projects", { data: args });
    return res.json();
  },
});

変更点:

  • this.projectIdはなくなり、IDはreturnsを介して返されるようになりました。後続のステップはfrom: { projectId: [createProjectStep, "id"] }を宣言してキーで読み取ります。nuka checkは実行前にバインディング順序を検証し、読み取りはその後続のステップ自身のレシートに表示されます(resultOfフィクスチャは、キー名で表現できないまれな読み取りのために引き続き利用可能です)。
  • 引数はスキーマキーにバインドされた名前付きキャプチャ({name:string} → args.name)であるため、パターンだけでどのテキストがどのフィールドになるかがわかります。
  • mutates: trueは、コードがたまたま行うことに関する事実ではなく、宣言になりました。nuka checkはこのステップがThenにバインドされた場合に警告し、読み取り専用環境はそのステップの実行を完全に拒否します。レシートは実行が実際に送信したものを記録しますが、そのカウントはステップが拒否される原因にはなりません。
  • this.requestは、最初の引数を分割構造化して名前が付けられたrequestフィクスチャになります。エグゼキュータが実際に注入するものだけがそこで名前付けでき、暗黙的なものはなく、実際に名前が付けられた名前だけが構築されます。pageもcontextも名前付けしないステップは、ブラウザを起動しません。

これが唯一の作業例です。compatと型付きの間のすべてのギャップのカタログではありません。別のパターンが頻繁に発生して例が必要になった場合、それは個別の追加であり、この単一のケースから即興で作り出すものではありません。

グローバルなAfterフックがクリーンアップだった場合

cucumber-jsスイートは、グローバルなAfterフックに依存して、ステップが作成したもの(テナント、シード行、一時ファイル)をクリーンアップすることがよくあります。これは、作成したステップが昇格されたら再検討する価値があります。リソースをnukadoko.config.tsの独自のfixturesでフィクスチャとして宣言し、クリーンアップをリソースを構築したのと同じ関数内のawait use(...)の直後に記述します。どのシナリオが実際にそれを必要とするかをタグだけで推測しなければならないフックに残すのではありません。nukadokoパッケージのdefineFixturesはフィクスチャを完全に型付けされたままに保ちます。型付きステップは、pageやrequestと同じ方法で、名前を分割構造化することでそれにアクセスします。これは追加可能なものであり、必須ではありません。型付きステップがまだ要求していないものをクリーンアップするAfterフックは、ステージ1と同じように依然として有効です。

型付きステップ形式のDSLからの移行

フィーチャーファイルやcucumberグルーがない場合、上記のステージ1のcompatドアは関係ありません。そのままスキップしてください。

これを可能にするのは、defineStepでpatternがオプションであることです。パターンなしでステップを定義しても、CLIのみの完全な語彙として機能し、nuka doで実行可能で、nuka describeで検査可能です。これは、同じ原則から生まれた異なる段階のペアです。まず各ステップを型付きdefineStepに移動し、その後、フィーチャーファイルが必要になったときにGherkinのpatternにバンドルします。

ソースのDSLがすでにdescription、args、returns、mutates、run関数のようなものを備えている場合、defineStepへの変換は直接行われます。それぞれにdefineStepの対応する場所があります。

やってはいけないこと

  • ステージ1とステージ2を同時に行わないでください。 インポートの切り替えと型付けの追加を同じ変更に混在させることは、まさに障害の原因を特定できなくする行為です。上記の「2つの段階、決して同時に行わないこと」を参照してください。
  • 移行途中でスイートが実行できなくなる状態にしないでください。 compatステップと型付きステップは同じフィーチャーファイル内で共存します。一部のステップが昇格され、他がまだcompatであるスイートは、常にテストに合格し続ける必要があります。「完全に型付けされること」を「実行できること」の前提条件にしないでください。
  • 型付きの代替が作成され、テストに合格するまで、compatグルーを削除しないでください。 新しいステップを作成し、nuka doで実行できるようにしてから、古いものを削除します。決して逆の順序では行わないでください。
  • nuka checkやnuka runが要求していない修正を推測しないでください。 これらのコマンドの出力は何が問題かの証拠です。それらがフラグを立てなかったものを変更することは、実際に壊れたものではなく、勘に基づいた変更です。

関連スキル