TypeScript

InversifyJSを利用したテスト

マナビト
マナビト
こんにちはマナビトです。
普段はフロントエンドエンジニアとしてJavaScript/TypeScript/Aangular/GraphQLを
メインに開発業務を行なっています。

バックエンドをTypeScriptで実装してみましたが、その際InversifyJSを使用した単体テストが非常に効率よく行うことができたので、本日はInversifyJSについて記事にしたいと思います。

DIとは何か

InversifyJSを述べる前にDIについて簡単に触れておきたいと思います。

DI(Dependency Injection)は直訳で「依存性の注入」と呼ばれ、依存対象となるオブジェクトのインスタンスをコンストラクタなどを通じて、外部から挿入するというデザインパターンのことを指します。外部から依存対象を注入することにより、クラスのコンストラクタの実装に依存することなく、ユニットテストを行うことができます。

依存性のあるプログラムは保守・テストがやりにくくその問題を解決することを目的としています。

DIコンテナ

DIコンテナとはDIにおける依存性の注入を実現するためのフレームワークです。本来インスタンスを生成する場合は、newを直接記載する必要がありますが、その場合至る所で依存関係が生まれてしまい、保守・テストの観点から見通しが非常に悪くなります。DIコンテナを使用する場合は、呼び出し元からインスタンスを呼び出すだけで、インスタンスを自動で渡してくれるという特徴があります。

この様なことから、DIコンテナを使用するメリットは以下の通りです。

・依存先の実装が終わっていなくても、開発に着手することができる。
・単体テストを行う際は、mockを作成して、値を埋め込めば実行可能。

class Examples {
    getData(): string {
        const retrieve: string = new RetrieveService();
        // 処理
        return retrieve;
    }
}

この様なコードを記載した場合、RetrieveServiceがnewされないと処理を実行することができず、RetrieveServiceのコンストラスタの実装に依存することになります。そのため、単体テストなどを実施したい場合、RetrieveServiceの実装も整備する必要があり、開発コストが増大してしまいます。この状態を解決するためにDIコンテナはnewする処理を一つに集約(コンテナ)することで依存関係を解消させることができます。

InversifyJSとは何か

InversifyJSはTypeScriptベースのDIコンテナライブラリです。TypeScriptベースなので、型を安全に実装することができます。

導入

まずはnpmでinversifyJSとreflect-metadataをインストールします。

npm install inversify reflect-metadata --save
yarn add inversify reflect-metadata   // yarnの場合
pnpm add inversify reflect-metadata   // pnpmの場合

inversifyJSを使用するために、tsconfig.jsonの中身を整備します。

{
    "compilerOptions": {
        "target": "es5",
        "lib": ["es6"],
        "types": ["reflect-metadata"],
        "module": "commonjs",
        "moduleResolution": "node",
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true
    }
}

これで導入は完了です。

InversifyJSを利用した単体テスト

インストールしたInversifyJSを使用してテストを実施してみます。

まずは核となるDIコンテナを生成してみたいと思います。

import { BindingScopeEnum, Container } from 'inversify';

export class DiUtil {

  private static containers: Container;

  static get container(): Container {
    if (DiUtil.containers === undefined) {
      DiUtil.containers = new Container({
        autoBindInjectable: true,
        defaultScope: BindingScopeEnum.Singleton
      });
    }
    return DiUtil.containers;
  }
}

containerが定義されてなければ、新たにコンテナを生成します。defaultScopeをBindingScopeEnum.Singletonとすることで、一つのインスタンスしか作ることができないように設計され、プログラム上で常に同一のインスタンスを参照するように制限します。
また、autoBindInjectable: trueにすることでインスタンスを取得します。

次にテスト対象となるメインのサービスを作成します。
とてもシンプルなコーディングですが、外から数字を渡して処理結果に基づいたbooleanが返ってきます。

import { injectable } from 'inversify';

@injectable()
export class testService {
  constructor() {}

  /**
   * ランダムでbooleanを返す
   * @param {number} data
   * @return {boolean}
   */
  testBook(data: number): boolean {
    return Math.random() < data ? true : false;
  }
}

次にテストコードを記載します。
resolveメソッドを使用して依存性を解決するようにします。

import 'reflect-metadata';
import { DiUtil } from '@/util/di.util';
import { testService } from '@/services/test';

describe(`testService`, (): void => {
  test(`testBook`, () => {
    const service = DiUtil.container.resolve(testService);
    const res = service.testBook(5);
    console.log(res);
  });
});

あとはJestで書かれたテストコードをデバッグできるように、VScodeの拡張機機能Jest Runnerをインストールします。

拡張機能をインストール後はRun/Debugの文字が表示されるようになるので、Runボタンを押します。

渡した値に対する処理結果が返ってきた場合はテスト完了です。

 console.log
   true

 at Object.<anonymous> (test/services/check-in/test.spec.ts:9:13)

 PASS  test/services/check-in/test.spec.ts (13.669 s)
  testService
    ✓ testBook (50 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        13.867 s

以上です。
ここまでお読みいただきありがとうございました。

COMMENT

メールアドレスが公開されることはありません。