Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

こちらのリンクで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
okumurakengo
人が作ってくれたご飯食べるときに何も言わずに食べるのは、ちょっとダメらしいという話を聞いたことがあるので、「あ、うめ、あ、うめ」って言いながら食ってたら、すごい変な人と思われてしまってしまった/初心者です、あまりわかっていません
https://bokete.jp/user/okumurakengo
qiitadon
Qiitadon(β)から生まれた Qiita ユーザー・コミュニティです。
https://qiitadon.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした