【问题标题】:How do I spyOn third party function with jest?我如何用玩笑监视第三方功能?
【发布时间】:2020-01-02 21:07:24
【问题描述】:

我无法模拟第三方依赖项。我总是收到这个错误:

无法窥探未定义的属性,因为它不是函数; 取而代之的是未定义

以下是此问题的详细信息。首先,这是我正在测试的功能:

文件:src/js/mp_wrapper.js

import { Viewer } from 'third-party';

module.exports = {
    createViewer: container => {
        if (util.isElement(container)) {
            return new Viewer(container);
        } else {
            throw new Error(
                'Invalid Element when attempting to create underlying viewer.',
            );
        }
    },
}

查看我第三方的源码,Viewer 很简单,长这样:


function Viewer(){
 // Doing things
}

Viewer.prototype.foo = function(){

}

module.exports = Viewer;

最后,这是我的测试。

文件:/tests/mp_wrapper.spec.js


import { Viewer } from 'third-party`;
import mp_wrapper from '../src/js/mp_wrapper';

describe('mp_wrapper', () => {
    describe('createViewer', () => {
        test('returns a new instance of the Viewer class', () => {
            const spy = jest.spyOn(Viewer).mockImplementation(() => jest.fn());
            // It fails on the line above... -> "Cannot spy the undefined property because it is not a function; undefined given instead"

            const testElement = document.createElement(testElement);
            let viewer = mp_wrapper.createViewer(testElement);

            expect(spy).toHaveBeenCalled();
            expect(viewer).toBeInstancecOf(Viewer);
            spy.mockRestore();
        });
    });
});

如何模拟和监视查看器本身?

我过去做过:

const spy = jest.spyOn(Viewer.prototype, 'foo').mockImplementation(() => jest.fn());

我也尝试过default,但没有成功:

const spy = jest.spyOn(Viewer, 'default').mockImplementation(() => jest.fn());

但现在我想监视 Viewer。

编辑:

这是我的最终解决方案。 @brian-lives-outdoors 答案是正确的,但我没有准确描述我的问题。我试图模拟的第三方库稍微复杂一些,因为它导出了一个包含多个构造函数的模块。它看起来像这样:

module.exports = {
  Viewer: require('./path/Viewer'),
  Foo: require('./foo_path/Foo'),
  Bar: require('./bar_path/Bar')
}

然后./path/Viewer 内部就像我之前描述的那样。

这就是我的解决方案最终的样子:

import { Viewer } from 'lib';
import mp_wrapper from '../src/js/mp_wrapper';

jest.genMockFromModule('lib');
jest.mock('lib');

describe('mp_wrapper', () => {
    describe('createViewer', () => {
        test('returns a new instance of the Viewer class and sets the local _viewer property', () => {
            const testContainer = document.createElement('div');
            const viewer = mp_wrapper.createViewer(testContainer);
            expect(Viewer).toHaveBeenCalledWith(testContainer); // Success!
            expect(viewer).toBeInstanceOf(Viewer); // Success!
        });
    });
});

@brian-lives-outdoors 我不明白的是,如果我注释掉上面的jest.mock('lib'); 行,它就不起作用了……为什么?

为什么genMockFromModule 本身不够用?

【问题讨论】:

  • @brian-lives-outdoors 对这个问题有什么想法吗?您似乎是模拟依赖项方面的专家

标签: javascript unit-testing jestjs


【解决方案1】:

示例代码混合了 ES6 import/ export 语法和 Node module.exports 语法...

...但基于如下所示的库:

lib.js

function Viewer() { }

Viewer.prototype.foo = function () { }

module.exports = Viewer;

...它会这样使用:

mp_wrapper.js

import Viewer from './lib';  // <= Babel allows Viewer to be used like an ES6 default export

export const createViewer = container => new Viewer(container);

...要监视Viewer,您需要在测试中模拟整个库:

mp_wrapper.spec.js

import Viewer from './lib';
import { createViewer } from './mp_wrapper';

jest.mock('./lib', () => jest.fn());  // <= mock the library

test('returns a new instance of the Viewer class', () => {
  const viewer = createViewer('the container');
  expect(Viewer).toHaveBeenCalledWith('the container');  // Success!
  expect(viewer).toBeInstanceOf(Viewer);  // Success!
});

请注意,如果库是 ES6 库,那么您可以像这样直接监视 default 导出:

import * as lib from './lib';

const spy = jest.spyOn(lib, 'default');  // <= spy on the default export

...但是由于 Babel 处理 ES6 和非 ES6 代码之间的互操作的方式,如果库不是 ES6,则这种方法不起作用。


编辑:对后续问题的回复

jest.genMockFromModule 生成模块的模拟版本并返回它。

例如:

const mock = jest.genMockFromModule('lib');

...生成lib 的模拟版本并将其分配给mock。请注意,这并不意味着在测试期间需要lib 时将返回模拟。

jest.genMockFromModule 在创建手动模拟时很有用:

__mocks__/lib.js

const lib = jest.genMockFromModule('lib');  // <= generate a mock of the module
lib.someFunc.mockReturnValue('some value');  // <= modify it
module.exports = lib;  // <= export the modified mock

在您的最终解决方案中,您有以下两行:

jest.genMockFromModule('lib');
jest.mock('lib');

这一行:

jest.genMockFromModule('lib');

...实际上并没有做任何事情,因为它正在生成模块的模拟,但返回的模拟没有被用于任何事情。

这一行:

jest.mock('lib');

...告诉Jest 自动模拟lib 模块,并且是这种情况下唯一需要的行。

【讨论】:

  • 非常感谢!我非常感谢您对这个问题的回答/帮助。有关进一步的解决方案和奖金问题,请参见我上面的编辑:-)
  • 我刚刚添加了对奖金问题@calbear47的答案
  • 你就是那个男人!感谢您的解释。绝对有帮助
【解决方案2】:

这里有一个解决方案:

util.js

const util = {
  isElement() {}
};

module.exports = util;

View.js,第三方模块:

function Viewer() {
  // Doing things
  console.log('new viewer instance');
}

Viewer.prototype.foo = function() {};

module.exports = { Viewer };

my_wrapper.js:

const { Viewer } = require('./viewer');
const util = require('./util');

module.exports = {
  createViewer: container => {
    if (util.isElement(container)) {
      return new Viewer(container);
    } else {
      throw new Error('Invalid Element when attempting to create underlying viewer.');
    }
  }
};

单元测试:

const { Viewer } = require('./viewer');
const my_wrapper = require('./');
const util = require('./util');

jest.mock('./viewer', () => {
  return {
    Viewer: jest.fn()
  };
});

describe('mp_wrapper', () => {
  beforeEach(() => {
    jest.resetAllMocks();
  });
  describe('createViewer', () => {
    it('t1', () => {
      util.isElement = jest.fn().mockReturnValueOnce(true);
      let viewer = my_wrapper.createViewer('el');
      expect(util.isElement).toBeCalledWith('el');
      expect(viewer).toBeInstanceOf(Viewer);
    });

    it('t2', () => {
      util.isElement = jest.fn().mockReturnValueOnce(false);
      expect(() => my_wrapper.createViewer('el')).toThrowError(
        new Error('Invalid Element when attempting to create underlying viewer.')
      );
      expect(Viewer).not.toBeCalled();
    });
  });
});

单元测试结果:

PASS  src/stackoverflow/57712713/index.spec.js
  mp_wrapper
    createViewer
      ✓ t1 (6ms)
      ✓ t2 (5ms)

----------|----------|----------|----------|----------|-------------------|
File      |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
----------|----------|----------|----------|----------|-------------------|
All files |      100 |      100 |       50 |      100 |                   |
 index.js |      100 |      100 |      100 |      100 |                   |
 util.js  |      100 |      100 |        0 |      100 |                   |
----------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        4.134s, estimated 9s

【讨论】:

    猜你喜欢
    • 2017-07-14
    • 2019-05-08
    • 2018-12-30
    • 1970-01-01
    • 1970-01-01
    • 2020-07-04
    • 1970-01-01
    • 2018-06-21
    • 1970-01-01
    相关资源
    最近更新 更多