如何在Jest中模拟模块未被模拟时导入的命名函数

wgx48brx  于 2023-06-20  发布在  Jest
关注(0)|答案(8)|浏览(136)

我有以下模块,我试图在Jest中测试:

// myModule.js

export function otherFn() {
  console.log('do something');
}

export function testFn() {
  otherFn();

  // do other things
}

如上所示,它导出一些命名函数,重要的是testFn使用otherFn
在Jest中,当我为testFn编写单元测试时,我想模拟otherFn函数,因为我不希望otherFn中的错误影响我的testFn单元测试。我的问题是我不确定最好的方法是什么:

// myModule.test.js
jest.unmock('myModule');

import { testFn, otherFn } from 'myModule';

describe('test category', () => {
  it('tests something about testFn', () => {
    // I want to mock "otherFn" here but can't reassign
    // a.k.a. can't do otherFn = jest.fn()
  });
});

任何帮助/洞察力是赞赏。

doinxwow

doinxwow1#

jest.mock()内部使用jest.requireActual()

jest.requireActual(moduleName)

返回实际的模块而不是模拟模块,从而绕过对模块是否应接收模拟实现的所有检查。

示例

我更喜欢这种简洁的用法,你需要在返回的对象中传播:

// myModule.test.js

import { otherFn } from './myModule.js'

jest.mock('./myModule.js', () => ({
  ...(jest.requireActual('./myModule.js')),
  otherFn: jest.fn()
}))

describe('test category', () => {
  it('tests something about otherFn', () => {
    otherFn.mockReturnValue('foo')
    expect(otherFn()).toBe('foo')
  })
})

Jest的Manual Mocks文档中也引用了此方法(靠近 * Examples * 的结尾):
为了确保手动mock和它的真实实现保持同步,在导出它之前,要求真实模块在手动mock中使用jest.requireActual(moduleName)并使用mock函数对其进行修改可能是有用的。

wz3gfoph

wz3gfoph2#

看来我来晚了,但这是可能的.
testFn只需要使用模块 * 调用otherFn *。
如果testFn使用该模块调用otherFn,则可以模拟otherFn的模块导出,testFn将调用模拟。
下面是一个工作示例:
myModule.js

import * as myModule from './myModule';  // import myModule into itself

export function otherFn() {
  return 'original value';
}

export function testFn() {
  const result = myModule.otherFn();  // call otherFn using the module

  // do other things

  return result;
}

myModule.test.js

import * as myModule from './myModule';

describe('test category', () => {
  it('tests something about testFn', () => {
    const mock = jest.spyOn(myModule, 'otherFn');  // spy on otherFn
    mock.mockReturnValue('mocked value');  // mock the return value

    expect(myModule.testFn()).toBe('mocked value');  // SUCCESS

    mock.mockRestore();  // restore otherFn
  });
});
sauutmhj

sauutmhj3#

import m from '../myModule';

这对我不起作用,我这样做了:

import * as m from '../myModule';

m.otherFn = jest.fn();
wvt8vs2t

wvt8vs2t4#

我知道这个问题很久以前就被问到了,但我只是遇到了这种情况,最终找到了一个可行的解决方案。所以我想在这里分享一下。
对于模块:

// myModule.js

export function otherFn() {
  console.log('do something');
}

export function testFn() {
  otherFn();

  // do other things
}

您可以更改为以下内容:

// myModule.js

export const otherFn = () => {
  console.log('do something');
}

export const testFn = () => {
  otherFn();

  // do other things
}

将它们作为常量而不是函数导出。我相信这个问题与JavaScript中的提升有关,使用const可以防止这种行为。
然后在你的测试中,你可以有如下内容:

import * as myModule from 'myModule';

describe('...', () => {
  jest.spyOn(myModule, 'otherFn').mockReturnValue('what ever you want to return');

  // or

  myModule.otherFn = jest.fn(() => {
    // your mock implementation
  });
});

模拟现在应该可以正常工作了。

cnwbcb6i

cnwbcb6i5#

编译后的代码将不允许babel检索otherFn()所引用的绑定。如果你使用一个函数表达式,你应该能够实现mocking otherFn()

// myModule.js
exports.otherFn = () => {
  console.log('do something');
}

exports.testFn = () => {
  exports.otherFn();

  // do other things
}
// myModule.test.js
import m from '../myModule';

m.otherFn = jest.fn();

但是正如@kentcdodds在前面的评论中提到的,您可能不想模拟otherFn()。相反,只需为otherFn()编写一个新的规范,并模拟它所进行的任何必要调用。
例如,如果otherFn()正在发出一个http请求...

// myModule.js
exports.otherFn = () => {
  http.get('http://some-api.com', (res) => {
    // handle stuff
  });
};

在这里,您可能希望模拟http.get并基于模拟的实现更新Assert。

// myModule.test.js
jest.mock('http', () => ({
  get: jest.fn(() => {
    console.log('test');
  }),
}));
nxowjjhe

nxowjjhe6#

基于Brian Adams' answer,这就是我如何能够在TypeScript中使用相同的方法。此外,使用jest.doMock()可以只在测试文件的某些特定测试中模拟模块函数,并为每个模块函数提供单独的模拟实现。

src/module.ts

import * as module from './module';

function foo(): string {
  return `foo${module.bar()}`;
}

function bar(): string {
  return 'bar';
}

export { foo, bar };

test/module.test.ts

import { mockModulePartially } from './helpers';

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

const { foo } = module;

describe('test suite', () => {
  beforeEach(function() {
    jest.resetModules();
  });

  it('do not mock bar 1', async() => {
    expect(foo()).toEqual('foobar');
  });

  it('mock bar', async() => {
    mockModulePartially('../src/module', () => ({
      bar: jest.fn().mockImplementation(() => 'BAR')
    }));
    const module = await import('../src/module');
    const { foo } = module;
    expect(foo()).toEqual('fooBAR');
  });

  it('do not mock bar 2', async() => {
    expect(foo()).toEqual('foobar');
  });
});

test/helpers.ts

export function mockModulePartially(
  modulePath: string,
  mocksCreator: (originalModule: any) => Record<string, any>
): void {
  const testRelativePath = path.relative(path.dirname(expect.getState().testPath), __dirname);
  const fixedModulePath = path.relative(testRelativePath, modulePath);
  jest.doMock(fixedModulePath, () => {
    const originalModule = jest.requireActual(fixedModulePath);
    return { ...originalModule, ...mocksCreator(originalModule) };
  });
}

模块的模拟函数被移动到位于单独文件中的辅助函数mockModulePartially中,因此可以从不同的测试文件(通常可以位于其他目录中)使用它。它依赖于expect.getState().testPath来固定到被模拟的模块(modulePath)的路径(使其相对于包含mockModulePartiallyhelpers.ts)。作为第二个参数传递给mockModulePartiallymocksCreator函数应该返回模块的模拟。此函数接收originalModule,模拟实现可以选择依赖它。

bxpogfeg

bxpogfeg7#

我用我在这里找到的混合答案解决了我的问题:
myModule.js

import * as myModule from './myModule';  // import myModule into itself

export function otherFn() {
  return 'original value';
}

export function testFn() {
  const result = myModule.otherFn();  // call otherFn using the module

  // do other things

  return result;
}

myModule.test.js

import * as myModule from './myModule';

describe('test category', () => {
  let otherFnOrig;

  beforeAll(() => {
    otherFnOrig = myModule.otherFn;
    myModule.otherFn = jest.fn();
  });

  afterAll(() => {
    myModule.otherFn = otherFnOrig;
  });

  it('tests something about testFn', () => {
    // using mock to make the tests
  });
});
dgjrabp2

dgjrabp28#

在第一个答案的基础上,你也可以使用babel-plugin-rewire来模拟导入的命名函数。您可以从表面上查看这一节,了解命名函数的重新连接。
对于您的情况,这里的一个直接好处是您不需要更改从您的函数调用其他函数的方式。

相关问题