🍘

NestJS の Injectable なクラスをサクっとテスト(動作確認)したい

2021/11/26に公開

「APIにリクエストするRepository書いたはいいけど、どうやって動作確認するんや...Controllerまで実装してテスト書かなきゃいけないの?」という疑問に対応した記録です。

TL;DR

Test.createTestingModule を使って必要なクラスをすべてDIした上で実行すれば動作確認できます。

何が課題か

外部APIへリクエストするためのHTTPモジュールのインスタンスをどうやって作ればいいかわかりませんでした。

newすればいいやん、という話なのですが、ひとつ考えるべきことがあります。NestJSは、@Injectableなクラスのインスタンス化はフレームワークにまかせてくれ、と言っています。他の言語のフレームワークではリクエストごとにスレッドが生成されるマルチスレッドベースのものもありますが、Node.jsの場合はそうではありません。したがって、基本的に状態をもたないシングルトンオブジェクトをつかいまわすのが安全だ、だそうです。

Remember that Node.js doesn't follow the request/response Multi-Threaded Stateless Model in which every request is processed by a separate thread. Hence, using singleton instances is fully safe for our applications.

https://docs.nestjs.com/fundamentals/injection-scopes

アプリケーションを構築する上ではありがたい話ですね。ですが他のモジュールに依存している@Injectableなクラスを動かしたいときには障壁になります。例として、スクレイピングなどで、クラスメソッドのポータルサイトのHTMLをとってくるRepositoryの例をみてみましょう。モジュール構成は次のようになっています。

src/components/classmethod/
├── classmethod.controller.ts
├── classmethod.service.ts
└── repositories
    └── classmethod.repository.ts

リポジトリに外部API(今回は単なるトップページのHTMLですが)へリクエストする処理を書きます。

yarn add @nestjs/axios # HTTPモジュールを利用するため
yarn add rxjs # HTTPモジュールのgetメソッドなどは`Observable`で返ってきます(このあたりもAngularインスパイアードがかいまみえる)。これをPromiseへ変換するため
classmethod.repository.ts
import { HttpService } from '@nestjs/axios';
import { Injectable } from '@nestjs/common';
import { firstValueFrom } from 'rxjs';

@Injectable()
export class ClassmethodRepository {
  constructor(private client: HttpService) {}

  async top(): Promise<string> {
    const url = `https://classmethod.jp`;
    const res = await firstValueFrom(this.client.get(url));
    return res.data;
  }
}

なんとなく動きそうではあるが、ちゃんと動くのか確認したい、実際どういうデータが返ってくるのかちょっと覗きたい。けど、このクラスが依存しているHttpServiceってどうやってDIすればいいの…?となりました。

そんなときは Test.createTestingModuleを使えばうまくいきました。

動作確認用のモジュールを作ってts-nodeで実行

テストではなく動作確認目的(捨てるコード)なので同じファイルに書いてしまいます。

classmethod.repository.ts
import { HttpModule, HttpService } from '@nestjs/axios';
import { Injectable } from '@nestjs/common';
+import { Test } from '@nestjs/testing';
import { firstValueFrom } from 'rxjs';

@Injectable()
export class ClassmethodRepository {
  constructor(private client: HttpService) {}

  async top(): Promise<string> {
    const url = `https://classmethod.jp`;
    const res = await firstValueFrom(this.client.get(url));
    console.log(res.data);
    return res.data;
  }
}


+ async function test() {
+     const moduleRef = await Test.createTestingModule({
+         imports: [HttpModule], // HttpModule(Axios)のHttpServiceを使いたいのでNestJSにインスタンスを作ってもらう
+         providers: [ClassmethodRepository], // 自分で定義したこのクラスのインスタンスを使いたいから作ってもらう
+     }).compile()
+ 
+     const repo = moduleRef.get<ClassmethodRepository>(ClassmethodRepository); // 作ってもらったインスタンスを取り出す
+     console.log(await repo.top()); // どんなデータが取れてるかな?
+ }
+ test();

冒頭で、NestJSは@Injectableなクラスを自動でインスタンシエートしてくれるという話をしました。動作確認にも同じ動きを再現する必要があり、そのためにテスト用のモジュールを作ることをやっているのが上記のコードです。実行すると結果が得られることがわかります。

> yarn ts-node src/components/classmethod/repositories/classmethod.repository.ts

...
      <li class="footer__item">
        <dl>
          <dt><a href="/services/">サービス</a></dt>
          <dd><a href="/services/members/">AWS総合支援</a></dd>
          <dd><a href="/services/line/">LINEサービス総合支援</a></dd>
          <dd><a href="/services/cx/">アプリケーション開発</a></dd>
          <dd><a href="/services/data-analytics/">データ分析環境構築支援</a></dd>
          <dd><a href="/partner/">SaaS導入コンサルティング</a></dd>
        </dl>
      </li>
...

@InjectableなクラスはTest.createTestingModule を使って必要なクラスをすべてDIした上で実行すれば動作確認できそうです。Rails ConsoleみたいなやつNestJSにもください。

調べるにあたり苦労したこと

=> NestJSでコマンドラインツールが作りたいわけじゃないんです。これはこれで面白そう。

https://www.reddit.com/r/Nestjs_framework/comments/niiu7x/rails_console_equivalent/

=> これがあきらめきれずに少し時間をかけすぎました。いまのところ、Rails Console のようなものを期待するのは難しそうですね。

https://docs.nestjs.com/fundamentals/testing#testing-utilities

=> テストの記述も調べましたが、わりとどの説明も「外部APIやデータベースアクセスをモックにして純粋なロジックをテストしよう」という方向に寄っていてRepositoryそのものを動作確認する方法がなかなか見つけられませんでした。

参考

https://docs.nestjs.com/fundamentals/testing#testing-utilities

Discussion