Quantcast
Channel: Node.jsタグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 8835

NestJS でダミーの Service を注入し、外部依存のないテストを実行する

$
0
0

この記事は NestJS アドベントカレンダー 4 日目の記事です。

はじめに

先日は Module と DI について説明しましたが、本日はもう一歩進んだ DI を活用したテストを実施してみます。
なお、サンプルでは MySQL に接続したり Docker を使用したりしていますが、怖がらないでください。
この記事では MySQL や Docker に依存せずにテストできるようにするテクニックを説明します。

サンプルコードのリポジトリは以下になります。

https://github.com/nestjs-jp/advent-calendar-2019/tree/master/day4-inject-dummy-service-to-avoid-external-dependency

なお、環境は執筆時点での Node.js の LTS である v12.13.1 を前提とします。

サンプルアプリの雛形を作る

今回のサンプルとなるアプリケーションの雛形を cli を用いて作ってゆきます。

$ nest new day4-inject-dummy-service
$ nest g module items
$ nest g controller items
$ nest g service items

ItemsController には以下のように Post と Get を実装していきます。

import{Controller,Post,Body,Get}from'@nestjs/common';import{CreateItemDTO}from'./items.dto';import{ItemsService}from'./items.service';@Controller('items')exportclassItemsController{constructor(privatereadonlyitemsService:ItemsService){}@Post()asynccreateItem(@Body(){title,body,deletePassword}:CreateItemDTO){constitem=awaitthis.itemsService.createItem(title,body,deletePassword,);returnitem;}@Get()asyncgetItems(){constitems=awaitthis.itemsService.getItems();returnitems;}}

ItemsService も雛形を作成します。

@Injectable()asynccreateItem(title:string,body:string,deletePassword:string){return;}asyncgetItems(){return[];}}

MySQL にデータを書き込む箇所を実装する

今回は Service の外部依存先として、 MySQL を例にあげます。
MySQL に接続するため、以下のライブラリをインストールします。

$ yarn add typeorm mysql

なお、今回は TypeORM の複雑な機能は極力使用せずにサンプルを記述します。
TypeORM についての説明や NestJS との組み合わせ方については別の記事で説明します。
また、本来は constructor で非同期の初期化を行うべきではないのですが、回避策は複雑なので、こちらも別途説明します。

@Injectable()exportclassItemsService{connection:Connection;constructor(){createConnection({type:'mysql',host:'0.0.0.0',port:3306,username:'root',database:'test',}).then(connection=>{this.connection=connection;}).catch(e=>{throwe;});}// connection が確率していないタイミングがあるため待ち受けるprivateasyncwaitToConnect(){if(this.connection){return;}awaitnewPromise(resolve=>setTimeout(resolve,1000));awaitthis.waitToConnect();}asynccreateItem(title:string,body:string,deletePassword:string){if(!this.connection){awaitthis.waitToConnect();}awaitthis.connection.query(`INSERT INTO items (title, body, deletePassword) VALUE (?, ?, ?)`,[title,body,deletePassword],);}asyncgetItems(){if(!this.connection){awaitthis.waitToConnect();}constrawItems=awaitthis.connection.query('SELECT * FROM items');constitems=rawItems.map(rawItem=>{constitem={...rawItem};deleteitem.deletePassword;returnitem;});returnitems;}}

また、 MySQL を Docker で立ち上げます。

$ docker-compose up

Docker ではない MySQL で実行する場合、 MySQL に testデータベースを作り、 create-table.sqlを流してください。

この状態でアプリケーションを起動してみましょう。MySQL が起動していれば、無事起動するはずです。

$ yarn start:dev

続いて curl でアプリケーションの動作確認をしてみます。

$ curl -XPOST-H'Content-Type:Application/json'-d'{"title": "hoge", "body": "fuga", "deletePassword": "piyo"}' localhost:3000/items
$ curl locaohost:3000/items
[{"title":"hoge","body":"fuga"}]

無事保存できるアプリケーションができました。

MySQL がない状態でもテストできるようにする

アプリケーションができたので、Mock を使ってテストを記述します。

前回までのサンプルでは特に DI を意識する必要がなかったため new ItemsService()としてテストを記述していましたが、
今回は DI に関連するため、 cli で自動生成される雛形にも用いられている Testモジュールを使用します。

describe('ItemsController',()=>{letitemsController:ItemsController;letitemsService:ItemsService;beforeEach(async()=>{consttestingModule:TestingModule=awaitTest.createTestingModule({imports:[ItemsModule],}).compile();itemsService=testingModule.get<ItemsService>(ItemsService);itemsController=newItemsController(itemsService);});describe('/items',()=>{it('should return items',async()=>{expect(awaititemsController.getItems()).toHaveLength(1);});});});

さて、この状態でテストを実行するとどうなるでしょうか。
MySQL を起動している場合はそのままテストが通りますが、 MySQL を停止すると以下のようにテストが落ちてしまいます。

$ jest
 PASS  src/app.controller.spec.ts
 FAIL  src/items/items.controller.spec.ts
  ● ItemsController › /items › should return items

    connect ECONNREFUSED 0.0.0.0:3306

          --------------------
      at Protocol.Object.<anonymous>.Protocol._enqueue (../node_modules/mysql/lib/protocol/Protocol.js:144:48)
      at Protocol.handshake (../node_modules/mysql/lib/protocol/Protocol.js:51:23)
      at PoolConnection.connect (../node_modules/mysql/lib/Connection.js:119:18)
      at Pool.Object.<anonymous>.Pool.getConnection (../node_modules/mysql/lib/Pool.js:48:16)
      at driver/mysql/MysqlDriver.ts:869:18
      at MysqlDriver.Object.<anonymous>.MysqlDriver.createPool (driver/mysql/MysqlDriver.ts:866:16)
      at MysqlDriver.<anonymous> (driver/mysql/MysqlDriver.ts:337:36)
      at step (../node_modules/tslib/tslib.js:136:27)
      at Object.next (../node_modules/tslib/tslib.js:117:57)

Test Suites: 1 failed, 1 passed, 2 total
Tests:       1 failed, 1 passed, 2 total
Snapshots:   0 total
Time:        1.204s, estimated 3s

ItemsService を Mock していますが、 ItemsService の初期化自体はされており、初期化処理の中で MySQL への接続しようとしているのが原因です。
このような、 外部へ依存する Provider の初期化をテストから除外するために、 ItemsService を上書きした状態で testingModuleを生成する機能が NestJS には備わっています。

以下のように DummyItemsService class を定義し、 overrideProviderを使って上書きします。

classDummyItemsService{asynccreateItem(title:string,body:string,deletePassword:string){return;}asyncgetItems(){constitem={id:1,title:'Dummy Title',body:'Dummy Body',};return[item];}}describe('ItemsController',()=>{letitemsController:ItemsController;letitemsService:ItemsService;beforeEach(async()=>{constapp:TestingModule=awaitTest.createTestingModule({imports:[ItemsModule],}).overrideProvider(ItemsService).useClass(DummyItemsService).compile();itemsService=app.get<ItemsService>(ItemsService);itemsController=newItemsController(itemsService);});describe('/items',()=>{it('should return items',async()=>{expect(awaititemsController.getItems()).toHaveLength(1);});});});

useClass()の代わりに useValue()を使うことで、 class ではなく変数で上書きすることもできます。

この状態でテストを実行すると、 MySQL が起動していなくても問題なく通過します。

yarn run v1.19.0
$ jest
 PASS  src/items/items.controller.spec.ts
 PASS  src/app.controller.spec.ts

Test Suites: 2 passed, 2 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        2.406s
Ran all test suites.
✨  Done in 2.94s.

おわりに

この記事で NestJS の持つ強力な DI の機能をお伝えできたかと思います。
より詳細な内容は公式のドキュメントの E2E テストの項にあるので、合わせてご確認ください。
https://docs.nestjs.com/fundamentals/testing#end-to-end-testing

また、今回説明できなかった TypeORM との合わせ方や、非同期の初期化を必要とする Service の扱い方については、後日別の記事で説明します。

明日は @potato4dさんが ExceptionFilter についてお話する予定です。


Viewing all articles
Browse latest Browse all 8835

Trending Articles