web-dev-qa-db-ja.com

jestを使用して同じモジュール内の関数をモックする方法

次の例を正しくモックする最良の方法は何ですか?

問題は、インポート後、fooが元のモックされていないbarへの参照を保持することです。

module.js:

export function bar () {
    return 'bar';
}

export function foo () {
    return `I am foo. bar is ${bar()}`;
}

module.test.js:

import * as module from '../src/module';

describe('module', () => {
    let barSpy;

    beforeEach(() => {
        barSpy = jest.spyOn(
            module,
            'bar'
        ).mockImplementation(jest.fn());
    });


    afterEach(() => {
        barSpy.mockRestore();
    });

    it('foo', () => {
        console.log(jest.isMockFunction(module.bar)); // outputs true

        module.bar.mockReturnValue('fake bar');

        console.log(module.bar()); // outputs 'fake bar';

        expect(module.foo()).toEqual('I am foo. bar is fake bar');
        /**
         * does not work! we get the following:
         *
         *  Expected value to equal:
         *    "I am foo. bar is fake bar"
         *  Received:
         *    "I am foo. bar is bar"
         */
    });
});

ありがとう!

編集:私は変更することができます:

export function foo () {
    return `I am foo. bar is ${bar()}`;
}

export function foo () {
    return `I am foo. bar is ${exports.bar()}`;
}

しかし、これはpです。私の意見ではeveryいどこでもやる:/

44
Mark

fwiw、私が決めた解決策は、デフォルト引数を設定することにより、 dependency injection を使用することでした。

だから私は変わるだろう

export function bar () {
    return 'bar';
}

export function foo () {
    return `I am foo. bar is ${bar()}`;
}

export function bar () {
    return 'bar';
}

export function foo (_bar = bar) {
    return `I am foo. bar is ${_bar()}`;
}

これはコンポーネントのAPIに対する重大な変更ではありません。次の操作を行うことで、テストで簡単にbarをオーバーライドできます。

import { foo, bar } from '../src/module';

describe('module', () => {
    it('foo', () => {
        const dummyBar = jest.fn().mockReturnValue('fake bar');
        expect(foo(dummyBar)).toEqual('I am foo. bar is fake bar');
    });
});

これには、テストコードが若干優れているという利点もあります。

7
Mark

この問題は、バーの範囲の解決方法に関連しているようです。

一方では、module.js 2つの関数をエクスポートします(これら2つの関数を保持するオブジェクトの代わりに)。モジュールがエクスポートされる方法のため、エクスポートされたもののコンテナへの参照は、あなたが言及したようにexportsです。

一方、これらの関数を保持し、その関数の1つ(関数バー)を置き換えようとするオブジェクトのように、エクスポート(別名module)を処理します。

Foo実装をよく見ると、実際にはbar関数への固定参照を保持しています。

bar関数を新しい関数に置き換えたと思ったら、実際にmodule.test.jsのスコープ内の参照コピーを置き換えただけです

Fooが実際に別のバージョンのbarを使用するようにするには、2つの可能性があります。

  1. Module.jsで、fooメソッドとbarメソッドの両方を保持して、クラスまたはインスタンスをエクスポートします。

    Module.js:

    export class MyModule {
      function bar () {
        return 'bar';
      }
    
      function foo () {
        return `I am foo. bar is ${this.bar()}`;
      }
    }
    

    fooメソッドでのthisキーワードの使用に注意してください。

    Module.test.js:

    import { MyModule } from '../src/module'
    
    describe('MyModule', () => {
      //System under test :
      const sut:MyModule = new MyModule();
    
      let barSpy;
    
      beforeEach(() => {
          barSpy = jest.spyOn(
              sut,
              'bar'
          ).mockImplementation(jest.fn());
      });
    
    
      afterEach(() => {
          barSpy.mockRestore();
      });
    
      it('foo', () => {
          sut.bar.mockReturnValue('fake bar');
          expect(sut.foo()).toEqual('I am foo. bar is fake bar');
      });
    });
    
  2. あなたが言ったように、グローバルexportsコンテナのグローバル参照を書き換えます。 エクスポートを初期状態に適切にリセットしないと、他のテストで奇妙な動作を引き起こす可能性があるため、これは推奨される方法ではありません。

19
John-Philip

別の解決策は、モジュールを独自のコードファイルにインポートし、エクスポートされたすべてのエンティティのインポートされたインスタンスを使用することです。このような:

import * as thisModule from './module';

export function bar () {
    return 'bar';
}

export function foo () {
    return `I am foo. bar is ${thisModule.bar()}`;
}

barfooのエクスポートされたインスタンスも使用しているため、barのモックは非常に簡単です。

import * as module from '../src/module';

describe('module', () => {
    it('foo', () => {
        spyOn(module, 'bar').and.returnValue('fake bar');
        expect(module.foo()).toEqual('I am foo. bar is fake bar');
    });
});

モジュールを独自のコードにインポートすると奇妙に見えますが、ES6の循環インポートのサポートにより、非常にスムーズに機能します。

15
MostafaR

この同じ問題があり、プロジェクトのリンティング標準のため、クラスを定義するか、exportsの参照を書き換えることは、リンティング定義によって禁止されていなくても、コードレビューの承認可能なオプションではありませんでした。実行可能なオプションとして私がつまずいたのは、 babel-rewire-plugin を使用することです。これは私がアクセスした別のプロジェクトで使用されていることがわかりましたが、リンクされている同様の質問の答えに既にあることに気付きました here 。これは、参考のためにリンクされた回答から提供されたこの質問用に調整されたスニペットです(スパイを使用しません)(私は異教徒ではないため、スパイを削除することに加えてセミコロンも追加しました):

import __RewireAPI__, * as module from '../module';

describe('foo', () => {
  it('calls bar', () => {
    const barMock = jest.fn();
    __RewireAPI__.__Rewire__('bar', barMock);
    
    module.foo();

    expect(bar).toHaveBeenCalledTimes(1);
  });
});

https://stackoverflow.com/a/45645229/686742

2
Brandon Hunter

エクスポートを定義すると、エクスポートオブジェクトの一部として関数を参照できます。その後、モックの関数を個別に上書きできます。これは、インポートがコピーではなく参照として機能するためです。

module.js:

exports.bar () => {
    return 'bar';
}

exports.foo () => {
    return `I am foo. bar is ${exports.bar()}`;
}

module.test.js:

describe('MyModule', () => {

  it('foo', () => {
    let module = require('./module')
    module.bar = jest.fn(()=>{return 'fake bar'})

    expect(module.foo()).toEqual('I am foo. bar is fake bar');
  });

})
1
Sean

私のために働く:

cat moduleWithFunc.ts

export function funcA() {
 return export.funcB();
}
export function funcB() {
 return false;
}

cat moduleWithFunc.test.ts

import * as module from './moduleWithFunc';

describe('testFunc', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  afterEach(() => {
    module.funcB.mockRestore();
  });

  it.only('testCase', () => {
    // arrange
    jest.spyOn(module, 'funcB').mockImplementationOnce(jest.fn().mockReturnValue(true));

    // act
    const result = module.funcA();

    // assert
    expect(result).toEqual(true);
    expect(module.funcB).toHaveBeenCalledTimes(1);
  });
});