LoginSignup
11
5

More than 3 years have passed since last update.

Node.jsのDependency injection:依存性注入の勉強をしてみた

Last updated at Posted at 2019-08-14

こちらのリンクでnodeを使ったDIについてわかりやすかったので、それをみながら勉強したというという投稿なります。
なのでリンク先を見ていただければ、この記事は見なくても大丈夫です。m(_ _)m
また、私はJavaScript勉強中のものです、間違いなどありましたら教えていただけるとありがたいです。

テスト対象のメソッド

callExternalService.js
const client = require('axios');

const externalServiceRoot = 'https://api.example-external-service.com';

async function callExternalService(anArgument) {
  const { status, data } = await client.post(`${externalServiceRoot}/an/endpoint`, anArgument);

  if (status !== 200) {
    throw new Error('Response doesn\'t look good');
  }

  return data;
}

exports.callExternalService = callExternalService;

こちらの関数を実行するたびに、リクエストが発生してしまいますのでaxiosのモック(スタブ)をDIします。
テストツールでjestを使って試しました。

$ yarn add -D jest
$ yarn jest -v
24.8.0

※jestのモックの機能を使えばDIしなくてもこのままでテスト可能なのですが、今回はDIを勉強したかったので、こちらの書き方は使っていません。(^ ^;)

jestのモック機能を使ってテストした例
callExternalService.test.js
const axios = require("axios");
const { callExternalService } = require("./callExternalService");

jest.mock("axios");

it("レスポンスが返り値になっていることの確認", async () => {
    axios.post.mockResolvedValue(Promise.resolve({
        status: 200,
        data: { hoge: "fuga" }
    }));
    expect(await callExternalService({})).toEqual({ hoge: "fuga" });
});

Monkey patch

まず、初期の頃のNode.jsでの書き方、this._client に axiosをのスタブを設定する方法。
このやり方だと、this._clientが正しい値になるように常にリセットなどしなければいけないので大変です。

const axios = require('axios');

const externalServiceRoot = 'https://api.example-external-service.com';

function ExternalServiceConstructor() {
  this._client = axios;
}

ExternalServiceConstructor.prototype.callExternalService = async function(anArgument) {
  const { status, data } = await this._client.post(`${externalServiceRoot}/an/endpoint`, anArgument);

  if (status !== 200) {
    throw new Error('Response doesn\'t look good');
  }

  return data;
}

exports.externalServiceConstructor = new ExternalServiceConstructor();
test
const { externalServiceConstructor } = require("./callExternalService");

it("レスポンスが返り値になっていることの確認", async () => {
    externalServiceConstructor._client = {
        post: () => Promise.resolve({
            status: 200,
            data: { hoge: "fuga" },
        })
    }

    expect(await externalServiceConstructor.callExternalService({}))
        .toEqual({ hoge: "fuga" });
});
$ yarn jest #テスト実行
 # ... (省略)
 PASS  ./callExternalService.test.js
  ✓ レスポンスが返り値になっていることの確認 (3ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.314s
Ran all test suites.
✨  Done in 2.30s.

Dependency injection

ここからDIの例です。最初の例として、引数でaxiosを渡すようにする方法です。

callExternalService.js
const client = require('axios');

const externalServiceRoot = 'https://api.example-external-service.com';

async function callExternalService(client, anArgument) {
  const { status, data } = await client.post(`${externalServiceRoot}/an/endpoint`, anArgument);

  if (status !== 200) {
    throw new Error('Response doesn\'t look good');
  }

  return data;
}

exports.callExternalService = callExternalService;
callExternalService.test.js
const { callExternalService } = require("./callExternalService");

it("レスポンスが返り値になっていることの確認", async () => {
    const client = {
        post: () => Promise.resolve({
            status: 200,
            data: { hoge: "fuga" },
        })
    }
    expect(await callExternalService(client, {})).toEqual({ hoge: "fuga" });
});

全ての依存関係を引数で渡す例

callExternalService.js
const client = require('axios');

const externalServiceRoot = 'https://api.example-external-service.com';

async function callExternalService(dependencies, anArgument) {
  const { status, data } = await dependencies.client.post(`${externalServiceRoot}/an/endpoint`, anArgument);

  if (status !== 200) {
    throw new Error('Response doesn\'t look good');
  }

  return data;
}

exports.callExternalService = callExternalService;

Function factory

カリー化して、axiosを渡す例。関数の戻り値で完成された関数を返すやり方。

callExternalService.js
const client = require('axios');

const externalServiceRoot = 'https://api.example-external-service.com';

function makeCallExternalService(client) {
    return async function callExternalService(anArgument) {
        const { status, data } = await client.post(`${externalServiceRoot}/an/endpoint`, anArgument);

        if (status !== 200) {
            throw new Error('Response doesn\'t look good');
        }

        return data;
    }
}

exports.makeCallExternalService = makeCallExternalService;
callExternalService.test.js
const { makeCallExternalService } = require("./callExternalService");

it("レスポンスが返り値になっていることの確認", async () => {
    const client = {
        post: () => Promise.resolve({
            status: 200,
            data: { hoge: "fuga" },
        })
    }
    expect(await makeCallExternalService(client)({}))
        .toEqual({ hoge: "fuga" });
});

Dependency injection container

awilixを使ったサンプルです。参考ページ通りにやるとこちらもうまく動かせました。

まず対象の関数を変更します。externalServiceRootも引数で渡すように変更しました。

callExternalService.js
function makeCallExternalService({ client, externalServiceRoot }) {
    return async function callExternalService(anArgument) {
        const { status, data } = await client.post(`${externalServiceRoot}/an/endpoint`, anArgument);

        if (status !== 200) {
            throw new Error('Response doesn\'t look good');
        }

        return data;
    }
}

exports.makeCallExternalService = makeCallExternalService;

container.jsを作成し、もろもろ設定します。

container.js
const { createContainer, asFunction, asValue } = require('awilix');
const axios = require('axios');
const { makeCallExternalService } = require("./callExternalService")

const container = createContainer();

container.register({
    callExternalService: asFunction(makeCallExternalService)
})

container.register({
    client: asValue(axios)
});

container.register({
    externalServiceRoot: asFunction(() => {
        if (!process.env.EXTERNAL_SERVICE_ROOT) {
            throw new Error('EXTERNAL_SERVICE_ROOT is not defined.')
        }

        return process.env.EXTERNAL_SERVICE_ROOT;
    })
});
// container.register({
//     externalServiceRoot: asValue("https://api.example.com")
// });
// ↑のような書き方でもOK
// 今回のようにロジックを含めたい場合は asFunction() を使う

module.exports = container;

普通に実行したい場合

container.js で設定したclientexternalServiceRootがそのままmakeCallExternalServiceの引数に入ってくれて(すごい)、それを実行するだけで使えます。

index.js
const container = require('./container');
const callExternalService = container.resolve('callExternalService');

(async () => {
    console.log(await callExternalService(argument = {}));
})()
bash
$ EXTERNAL_SERVICE_ROOT="https://api.example-external-service.com" node index.js

モックして使う場合

先ほどはcontainer.resolve('callExternalService');としてcallExternalServiceを取得しましたが、モックするときはcontainerの機能を使わずにそのままmakeCallExternalServiceからcallExternalServiceを作成するとうまくいくようです。

callExternalService.test.js
const { makeCallExternalService } = require("./callExternalService");

const client = {
    post: () => Promise.resolve({ status: 200, data: { hoge: "fuga" } })
}
const callExternalService = makeCallExternalService({
    externalServiceRoot: 'FAKE_ROOT',
    client,
})

it("レスポンスが返り値になっていることの確認", async () => {
    expect(await callExternalService({})).toEqual({ hoge: "fuga" });
});

awilixを初めてさわってみたのでとても勉強になりました。
最後まで読んでいただいてありがとうございました。m(_ _)m

  • 確認バージョン
    • node 12.7.0
    • jest 24.8.0
    • awilix 4.2.2
11
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
5