他のモジュールを必要とするNode.jsモジュールを単体テストする方法と、グローバルなrequire関数をモックする方法は?


156

これは私の問題の核心を説明する簡単な例です:

var innerLib = require('./path/to/innerLib');

function underTest() {
    return innerLib.doComplexStuff();
}

module.exports = underTest;

このコードの単体テストを記述しようとしています。関数を完全にモックアウトinnerLibせずに、の要件をモックアウトするにはどうすればよいrequireですか?

だから私はグローバルをモックアウトしようとrequireしていて、それをやってもうまくいかないことを見つけようとしています:

var path = require('path'),
    vm = require('vm'),
    fs = require('fs'),
    indexPath = path.join(__dirname, './underTest');

var globalRequire = require;

require = function(name) {
    console.log('require: ' + name);
    switch(name) {
        case 'connect':
        case indexPath:
            return globalRequire(name);
            break;
    }
};

問題はrequireunderTest.jsファイル内の関数が実際にモックアウトされていないことです。それはまだグローバルrequire関数を指しています。したがってrequire、モックインしているのと同じファイル内でのみ関数をモックアウトできるようです。requireローカルコピーをオーバーライドした後でも、グローバルを使用して何かを含める場合は、必要なファイルにはまだグローバルrequire参照。


上書きする必要がありますglobal.requiremoduleモジュールはモジュールスコープなので、変数はデフォルトで書き込まれます。
レイノス

@Raynosどうすればいいですか?global.requireは未定義ですか?私がそれを自分の関数に置き換えたとしても、他の関数は決してそれらを使用しないでしょうか?
HMR

回答:


175

今はできる!

私は、あなたがそれをテストしている間、あなたのモジュールの中でグローバルな要求を上書きすることを処理するproxyquireを公​​開しました。

つまり、必要なモジュールのモックを挿入するためにコード変更する必要はありません

Proxyquireには非常にシンプルなAPIがあり、テストしようとしているモジュールを解決して、必要なモジュールのモック/スタブを1つの簡単な手順で渡すことができます。

@Raynosは、それを実現するため、または代わりにボトムアップ開発を行うために、従来はあまり理想的ではないソリューションに頼らなければならなかったのは正しい

これが、私がプロキシクワイアを作成した主な理由です-手間のかからないトップダウンのテスト駆動開発を可能にするためです。

ドキュメントと例を見て、ニーズに合うかどうかを判断してください。


5
私はproxyquireを使用していますが、良いことは十分に言えません。助かった!Appcelerator Titaniumで開発されたアプリの一部のモジュールを絶対パスおよび多くの循環依存関係に強制するjasmine-nodeテストを作成する必要がありました。proxyquireを使用すると、それらのギャップをなくし、各テストで不要な粗末を模擬できます。(ここで説明)。どうもありがとうございました!
スキマ

proxyquireがコードを適切にテストするのに役立ったと聞いてうれしいです:)
Thorsten Lorenz

1
とても素敵な@ThorstenLorenz、私はdefします。使っているproxyquire
bevacqua 2013年

素晴らしい!受け入れられない「できない」という答えを見たとき、「まあ、まじで?」と思った。しかし、これは本当にそれを救った。
チャドウィック2013年

3
Webpackを使用している場合は、proxyquireの調査に時間を費やさないでください。Webpackには対応していません。代わりにインジェクトローダーを調べています(github.com/plasticine/inject-loader)。
Artif3x 2017年

116

この場合のより良いオプションは、返されるモジュールのメソッドをモックすることです。

良くも悪くも、ほとんどのnode.jsモジュールはシングルトンです。同じモジュールを必要とする2つのコードは、そのモジュールへの同じ参照を取得します。

これを活用して、必要な項目を模擬するためにsinonなどを使用できます。 モカテストは次のとおりです。

// in your testfile
var innerLib  = require('./path/to/innerLib');
var underTest = require('./path/to/underTest');
var sinon     = require('sinon');

describe("underTest", function() {
  it("does something", function() {
    sinon.stub(innerLib, 'toCrazyCrap').callsFake(function() {
      // whatever you would like innerLib.toCrazyCrap to do under test
    });

    underTest();

    sinon.assert.calledOnce(innerLib.toCrazyCrap); // sinon assertion

    innerLib.toCrazyCrap.restore(); // restore original functionality
  });
});

Sinonはアサーションを作成するためにchaiと適切に統合されており、スパイ/スタブのクリーンアップを容易にするためにsinonとmocha統合するモジュールを作成しました(テスト汚染を回避するため)。

underTestは関数のみを返すため、同じ方法でunderTestをモックすることはできません。

別のオプションは、Jestモックを使用することです。彼らのページでフォローアップ


1
残念ながら、ここで説明するように、node.jsモジュールはシングルトンであることが保証されていません。justjs.com
FrontierPsycho

4
@FrontierPsychoいくつかのこと:まず、テストに関する限り、記事は無関係です。すべてのコードがrequire('some_module')同じnode_modules dirを共有しているため、依存関係をテストしている限り(依存関係の依存関係ではない)、すべてのコードは同じオブジェクトを取得します。第2に、この記事では名前空間とシングルトンを融合させています。第三に、その記事はかなり古く(node.jsに関する限り)、そのため、当時有効だったかもしれないものが現在は無効になっている可能性があります。
Elliot Foster

2
うーん。私たちの1人が実際にある点を証明するコードを掘り起こさない限り、依存関係注入のソリューション、または単にオブジェクトを渡すだけの方法をとるのであれば、それはより安全で将来の証明になります。
FrontierPsycho

1
何を証明することを求めているのかわかりません。ノードモジュールのシングルトン(キャッシュ)の性質は一般的に理解されています。依存性注入は良いルートですが、かなり多くのボイラープレートとより多くのコードになる可能性があります。DIは静的に型付けされた言語でより一般的であり、スパイ/スタブ/モックを動的にコードにダックパンチするのは困難です。過去3年間に私が行った複数のプロジェクトでは、上記の回答で説明した方法を使用しています。控えめに使用しますが、すべての方法の中で最も簡単です。
Elliot Foster

1
sinon.jsを読むことをお勧めします。(上記の例のように)sinonを使用している場合はinnerLib.toCrazyCrap.restore()、restubを実行するか、sinnonを呼び出しsinon.stub(innerLib, 'toCrazyCrap')てスタブの動作を変更できます innerLib.toCrazyCrap.returns(false)。また、再配線はproxyquire上記の拡張機能と非常によく似ているようです。
Elliot Foster

11

私はmock-requireを使用していますrequireテストするモジュールの前にモックを定義してください。


また、模擬したくないテストでキャッシュファイルが取得されないように、stop(<file>)またはstopAll()をすぐに実行することもお勧めします。
ジャスティンクルーゼ

1
これはトンを助けました。
Wallop

2

あざけることrequireは私にとって厄介なハックのように感じます。私は個人的にはそれを避け、コードをリファクタリングしてテストしやすくします。依存関係を処理するには、さまざまな方法があります。

1)依存関係を引数として渡します

function underTest(innerLib) {
    return innerLib.doComplexStuff();
}

これにより、コードは普遍的にテスト可能になります。欠点は、依存関係を渡す必要があるため、コードがより複雑に見える可能性があることです。

2)モジュールをクラスとして実装し、クラスのメソッド/プロパティを使用して依存関係を取得する

(これは、クラスの使用が合理的ではない、不自然な例ですが、アイデアを伝えます)(ES6の例)

const innerLib = require('./path/to/innerLib')

class underTestClass {
    getInnerLib () {
        return innerLib
    }

    underTestMethod () {
        return this.getInnerLib().doComplexStuff()
    }
}

これで、getInnerLibメソッドを簡単にスタブしてコードをテストできます。コードはより冗長になりますが、テストも簡単になります。


1
あなたが推測しているように、それはハッキーだとは思わない...これはあざけるの本質です。必要な依存関係をモックすると、物事が非常に簡単になり、コード構造を変更せずに開発者に制御を与えることができます。メソッドが冗長すぎるため、理由を説明するのが困難です。私はこれよりもproxyrequireまたはmock-requireを選択します。ここには問題はありません。コードはクリーンで簡単に推論でき、これを読んだほとんどの人は、複雑にさせたいコードをすでに書いています。これらのライブラリがハッキングされている場合、モックやスタブもあなたの定義によるハッキングであり、停止する必要があります。
Emmanuel Mahuni

1
アプローチ1の問題は、内部実装の詳細をスタックに渡すことです。複数の層があると、モジュールのコンシューマーになるのがはるかに複雑になります。依存関係が自動的に注入されるように、アプローチのようなIOCコンテナーで動作できますが、importsステートメントを介してノードモジュールに依存関係が既に注入されているので、そのレベルでそれらをモックできるのは理にかなっています。 。
magritte

1)これは単に問題を別のファイルに移動します2)まだ他のモジュールをロードしているため、パフォーマンスのオーバーヘッドを課し、副作用を引き起こす可能性がありcolorsます(をいじる人気のモジュールのようにString.prototype
ThomasR

2

jestを使用したことがあれば、jestのモック機能に慣れていることでしょう。

"jest.mock(...)"を使用すると、コード内のrequire-statementで発生する文字列をどこかに指定でき、その文字列を使用してモジュールが必要な場合は常に代わりにモックオブジェクトが返されます。

例えば

jest.mock("firebase-admin", () => {
    const a = require("mocked-version-of-firebase-admin");
    a.someAdditionalMockedMethod = () => {}
    return a;
})

「firebase-admin」のすべてのインポート/要件を、「factory」関数から返されたオブジェクトで完全に置き換えます。

まあ、jestは実行するすべてのモジュールの周りにランタイムを作成し、モジュールにrequireの「フック」バージョンを注入するため、jestを使用するときにそれを行うことができますが、jestなしではこれを行うことはできません。

私はモック必須でこれを達成しようとしましたが、私にとっては、ソース内のネストされたレベルでは機能しませんでした。githubで次の問題を確認してください:mock-requireは常にMochaで呼び出されるとは限りません

これに対処するために、私はあなたが望むものを達成するために使用できる2つのnpm-モジュールを作成しました。

1つのbabelプラグインとモジュールモッカーが必要です。

.babelrcで、次のオプションを使用してbabel-plugin-mock-requireプラグインを使用します。

...
"plugins": [
        ["babel-plugin-mock-require", { "moduleMocker": "jestlike-mock" }],
        ...
]
...

テストファイルでは、次のようにjestlike-mockモジュールを使用します。

import {jestMocker} from "jestlike-mock";
...
jestMocker.mock("firebase-admin", () => {
            const firebase = new (require("firebase-mock").MockFirebaseSdk)();
            ...
            return firebase;
});
...

jestlike-mockモジュールはまだ非常にルディメンタルで、文書の多くを持っていませんが、多くのコードのいずれかがありません。より完全な機能セットのPRに感謝します。目標は、「jest.mock」機能全体を再作成することです。

jestがどのように実装されているかを確認するには、「jest-runtime」パッケージのコードを検索します。たとえば、https://github.com/facebook/jest/blob/master/packages/jest-runtime/src/index.js#L734を参照してくださいここでは、モジュールの「オートモック」が生成されます。

それが役に立てば幸い;)


1

できません。最も低いモジュールが最初にテストされ、モジュールを必要とするより高いレベルのモジュールがその後にテストされるように、ユニットテストスイートを構築する必要があります。

また、サードパーティのコードとnode.js自体も十分にテストされていると想定する必要があります。

上書きするモックフレームワークが近い将来に届くのを見ると思います global.require

モックを注入する必要がある場合は、コードを変更してモジュールスコープを公開できます。

// underTest.js
var innerLib = require('./path/to/innerLib');

function underTest() {
    return innerLib.toCrazyCrap();
}

module.exports = underTest;
module.exports.__module = module;

// test.js
function test() {
    var underTest = require("underTest");
    underTest.__module.innerLib = {
        toCrazyCrap: function() { return true; }
    };
    assert.ok(underTest());
}

これは.__moduleAPIに公開され、コードはモジュール自体の危険にさらされてモジュールスコープにアクセスできます。


2
サードパーティのコードが十分にテストされていると仮定すると、IMOを機能させるための優れた方法ではありません。
henry.oswald

5
@beckそれは働くのに最適な方法です。すべての依存関係が十分にテストされるように、高品質のサードパーティコードのみを使用するか、コードのすべての部分を記述する必要があります
Raynos

わかりました、あなたがあなたのコードとサードパーティのコードとの間の統合テストを行わないことについて言及していると思いました。同意した。
henry.oswald

1
「単体テストスイート」は単体テストのコレクションにすぎませんが、単体テストは互いに独立している必要があるため、単体テストの単体です。ユニットテストを使用するには、ユニットテストが高速で独立している必要があります。これにより、ユニットテストが失敗したときにコードがどこで壊れているかを明確に確認できます。
Andreas Berheim Brudin 2015

これは私にはうまくいきませんでした。モジュールオブジェクトは、「VAR innerLib ...」など公開していません
AnitKryst

1

あなたはあざけるライブラリーを使うことができます:

describe 'UnderTest', ->
  before ->
    mockery.enable( warnOnUnregistered: false )
    mockery.registerMock('./path/to/innerLib', { doComplexStuff: -> 'Complex result' })
    @underTest = require('./path/to/underTest')

  it 'should compute complex value', ->
    expect(@underTest()).to.eq 'Complex result'

1

好奇心旺盛な人のためにモジュールをモックするシンプルなコード

これが秘密のソースなので、require.cacheand note require.resolveメソッドを操作する部分に注意してください。

class MockModules {  
  constructor() {
    this._resolvedPaths = {} 
  }
  add({ path, mock }) {
    const resolvedPath = require.resolve(path)
    this._resolvedPaths[resolvedPath] = true
    require.cache[resolvedPath] = {
      id: resolvedPath,
      file: resolvedPath,
      loaded: true,
      exports: mock
    }
  }
  clear(path) {
    const resolvedPath = require.resolve(path)
    delete this._resolvedPaths[resolvedPath]
    delete require.cache[resolvedPath]
  }
  clearAll() {
    Object.keys(this._resolvedPaths).forEach(resolvedPath =>
      delete require.cache[resolvedPath]
    )
    this._resolvedPaths = {}
  }
}

次のように使用

describe('#someModuleUsingTheThing', () => {
  const mockModules = new MockModules()
  beforeAll(() => {
    mockModules.add({
      // use the same require path as you normally would
      path: '../theThing',
      // mock return an object with "theThingMethod"
      mock: {
        theThingMethod: () => true
      }
    })
  })
  afterAll(() => {
    mockModules.clearAll()
  })
  it('should do the thing', async () => {
    const someModuleUsingTheThing = require('./someModuleUsingTheThing')
    expect(someModuleUsingTheThing.theThingMethod()).to.equal(true)
  })
})

しかし... proxyquireはかなり素晴らしいので、それを使用する必要があります。それはあなたの要求オーバーライドをテストのみにローカライズしたままにし、私はそれを強くお勧めします。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.