フレームワークの使い方やテスト手法に迷ったときにCursorのサポートを得た話

に公開

NestJSのようなフレームワークを使う開発では、時折「なぜこれが動くのか」「なぜこれが正しいのか」と悩むことがあります。今回はテストコードの修正過程で発生した疑問と、Cursorの生成AIを活用して納得解を見つけた体験を共有します。

発生した問題

あるコントローラーのユニットテストで以下のようなエラーが発生しました。

● Billings Controller › POST / › given {"message": "Your card is not supported.", "status": 400}, should return [BadRequestException: Your card is not supported.]
    CognitoService is marked as a scoped provider. Request and transient-scoped providers can't be used in combination with "get()" method. Please, use "resolve()" instead.
      37 |       controller = module.get<BillingsController>(BillingsController);
      38 |       billingService = module.get<BillingsService>(BillingsService);
    > 39 |       cognitoSeervice = module.get<CognitoService>(CognitoService);
         |                                ^
      40 |     });

単純な修正課題に見えましたが、よく調べてみると興味深い問題が隠れていました。

修正プロセス

まずはエラーメッセージの指示に従って対応してみました。エラーには「スコープ付きプロバイダーにはget()ではなくresolve()を使え」と書かれています。

     let cognitoSeervice: CognitoService;
     beforeEach(async () => {
       const module: TestingModule = await createModule();
-      controller = module.get<BillingsController>(BillingsController);
       billingService = module.get<BillingsService>(BillingsService);
-      cognitoSeervice = module.get<CognitoService>(CognitoService);
+      cognitoSeervice = await module.resolve<CognitoService>(CognitoService);
+      
+      // BillingsControllerを手動で作成し、モック済みのサービスを注入
+      controller = new BillingsController(billingService, cognitoSeervice);
     });

修正後、テストは無事通りました。しかし一般的なNestJSのテストパターンから外れた修正に見えたため、「本当にこれでいいのだろうか」という疑問が湧いてきました。

Cursorで徹底的に調査

そこでCursorのエージェント機能を使って調査することにしました。「なぜ一般的なテストパターンを使わずに、クラスを直接インスタンス化する必要があったのか」を納得いくまで質問しました。

Cursorは公式ドキュメントを参照しながら、わかりやすく説明してくれました。

スコープ付きプロバイダーの特性

NestJSでは、プロバイダーのスコープが3種類用意されています。

・DEFAULT (シングルトン): アプリケーション全体で共有される1つのインスタンスです。
・REQUEST: 各リクエストごとに新しいインスタンスが作成されます。
・TRANSIENT: 注入されるたびに新しいインスタンスが作成されます。

今回問題になったCognitoServiceはリクエストスコープ(scope: Scope.REQUEST)で定義されていたため、テストでは特別な扱いが必要だったのです。

テスト方法の違い

テスト方法には、二つのアプローチがありました。

  1. よく見かける方法では、module.get()を使ってDIコンテナからインスタンスを取得します。
  2. 今回採用した方法では、手動でインスタンス化して依存関係を明示的に制御しています。

一般的なパターンでの問題点

もし手動インスタンス化をせずに一般的なパターンを使おうとすると、このような問題が生じます。

controller = module.get<BillingsController>(BillingsController);
cognitoSeervice = await module.resolve<CognitoService>(CognitoService);
// このモックは効果がありません
jest.spyOn(cognitoSeervice, 'loadCurrentUser').mockImplementation(...);

この場合、モックが機能しない理由は明確です。module.get()で取得したコントローラーとmodule.resolve()で取得したサービスが別のインスタンスになってしまうためです。

納得の瞬間

AIの説明を聞いているうちに、今回の修正アプローチが正しそうであると感じてきました。

リクエストスコープのサービスはresolve()で取得する必要があります。また、テスト環境ではコントローラー内のサービスインスタンスとモック対象のインスタンスが一致していないと効果がありません。手動インスタンス化を行うことで、モック済みのサービスを確実にコントローラーに注入できるのです。

Cursorを活用した開発の価値

CursorなどのAIエージェントツールを利用することで、「納得がいくまで調査や説明を求めること」が可能となります。人間同士のコミュニケーションでは、お互いが持つ時間的制約などから、7回も8回も9回も質問を繰り返していくと、「この先はちょっと後でslackにてやろうか」のようなことになりがちです。AIエージェント相手に壁打ちをすることで、リソースの検索や引用も含めて、ある程度のラインまで納得のいく説明を要求できるのは大きな変化と言えるでしょう。

生成された結果が本当に正しいかを検証する必要はもちろんあります。この点についてもエージェントに引用元のリンクを要求し、その内容を目視でチェックするなどでカバーすることができます。

まとめ

Cursorのような生成AI搭載の開発ツールは、コード補完だけでなく、こういった概念理解や疑問解消にも大きな助けとなります。トークン消費が気になる場合は、Claude DesktopとClaude Code MCPを組み合わせるといった工夫も考えられます。

大切なのは「納得するまで質問する」という姿勢だと思います。そうした過程こそが、フレームワークやライブラリへの理解を深め、より良い開発者になるための学びになるのです。

デジタルキューブ

Discussion