web-dev-qa-db-ja.com

ノードモジュールをフレキシブル/テスト可能にするための立派なデザインパターン?

私は、自分よりも経験豊富なテスターから意見を求めています。 :)

ノードモジュールをテスト可能にして、rewireやmockeryなどのマジックライブラリを使用してモジュールのimport/requireステートメントをインターセプトすることなく、依存関係のスパイ/スタブ/モッキングを可能にしようとしています。

各モジュール内にファクトリー関数を作成するアプローチを使用することを考えていました。ファクトリ関数は、モジュールをエクスポートするために必要な依存関係を受け入れます。デフォルトのエクスポートでは、モジュールからの標準のインポート文を使用してファクトリ関数を呼び出します。また、テストで依存関係を注入できるように、ファクトリー関数自体もエクスポートします。

次に例を示します(これは、設計アプローチを強調するために完全に考案および簡略化されています)。

import { foo } from './utils/foo';

// We export our factory, exposing it to consumers.
export const factory = (dependencies = {}) => {
  const {
    $foo = foo // defaults to standard imp if none provided.
  } = dependencies;  

  return function bar() {
    return $foo();
  }
}

// The default implementation, which would end up using default deps.
export default factory();

考え?これは冗長すぎますか?同じことを達成するためのより良いメカニズムはありますか?私がプロジェクトに追加するボイラープレートの追加について少し心配していますが、おそらくトレードオフの価値があります。

7
ctrlplusb

一般的に、このようなものは良い考えです。私は特にJavaScriptの経験はあまりありませんが、他の言語をテストする経験はたくさんあります。そして、このようなコールバックフックを公開することは、アーキテクチャ全体を大幅に変更せずに依存性注入を可能にする非常に価値のある手法であることがわかりました。実際、最近このスタイルを提案する 記事 を書きましたC++アプリケーションの依存関係管理の概要。私のワークフローは次のようになります:

  • find関連する依存関係。特に独立してテストできる場合は、すべての依存関係をモックする必要はありません。ただし、データを挿入したり、テストスパイを挿入したり、高価な操作を模倣したりする場所を抽出してください。

  • describe依存関係によって提供されるサービス。おそらく、依存関係によって提供されるAPI全体を使用しているのではなく、ごく一部を使用しているだけです。その依存関係によって提供されるservicesは何ですか?この説明を単純なラッパーオブジェクトにキャプチャすることは理にかなっています。最も単純なケースでは、依存関係は単一のメソッド呼び出しのみにあり、より一般的なケースでは、抽出するサービスはファクトリ関数です。ファクトリーの問題は、製品のAPI全体をAPIに含めることです(これについては、 Demeter of Demeter を参照してください)。

  • extract外部からスワップ可能なコールバックへの依存関係。デフォルト値は依存関係に委任するだけです。そのコールバックへのアクセス方法は、さまざまな要因によって異なります。私はオプションの引数としてそれに依存する各関数にコールバックを渡すのが好きで、デフォルトのコールバックの使用は提供されていません。ただし、グローバル変数も機能します。妥協案は、モジュールのAPI全体をオブジェクトとして明示的に記述することです(Facadeパターンも参照)。これにより、グローバルな依存関係の管理は回避されますが、モジュールに対して事実上グローバルになります。

  • overrideテストの依存関係。

私のアプローチとあなたのコードの主な違いは、barFactoryジェネレーターを廃止することですが、このES6コードを正しく理解しているかどうかはわかりません。明らかになるモジュールパターンを備えたプレーンな古いJavaScriptでは、すべての依存関係を説明するオブジェクトをモジュールファサードに含めます。

var module = (function(){
  var deps = {};
  deps.frobnicateMyThing = function(thing) {
    staticDependency.frobnicate(thing);
  };

  function externallyVisibleApi() {
    ...
    deps.frobnicateMyThing(thing);
  }

  return {
    "externallyVisibleApi": externallyVisibleApi,
    "deps": deps,
  };
})();

...
// in test:
module.deps.frobnicateMyThing = function(thing) {
  assertIGotTheExpected(thing);
};

module.externallyVisibleApi();
4
amon

いくつかの異なるアプローチを試した後、私は非常にオブジェクト指向のノードモジュールを記述したいと思います。基本的にJavaなどから借用したもので、ファイルはモジュール、クラスです。

// MyModule.js
class MyModule {

    // provide dependencies in constructor
    constructor(dependencyA, dependencyB) {
        this.dependencyA = dependencyA;
        this.dependencyB = dependencyB;
    }

}

module.exports = MyModule;


// MyModuleTest.js
var MyModule = require("./MyModule");

describe("MyModule Test", function () {

    it("should do something", function () {
        var mockA = {};
        var mockB = {};
        var myModule = new MyModule(mockA, mockB);

        // work with myModule and mocks
    });

});

これにはいくつかの利点があります。

  • テストとモックが簡単になります
  • 統合テストの実際の依存関係を簡単に使用できます
  • テストにはグローバルな状態はありません。いつでも新鮮なインスタンスを操作し、オブジェクトをモックとして分離できます。 JavaScriptの従来のアプローチは、「グローバル」オブジェクトにスタブまたはスパイを作成することです。これはグローバル状態を導入します。これは本当に危険でエラーが発生しやすいです(私の経験では)。
  • IOCコンテナーインジェクションを含むコンテナーを本番環境で使用できます

IOC yaioc の例(免責事項:私は著者です):

// myapp.js
var container = yaioc.container();
container.register(require("./DependencyA");
container.register(require("./DependencyB");
container.register(require("./MyModule"));
// ...
var myModule = container.get("myModule");

私はこれをいくつかのプロジェクトで使用し、テストにおける優れた柔軟性とシンプルさに満足しています。オーバーヘッドはほとんどなく、覚えるのに複雑なモック/リセット/マジックはありません。

1
Bruno Schäpper