如何在单个测试的基础上更改模拟实现 [Jestjs]
Posted
技术标签:
【中文标题】如何在单个测试的基础上更改模拟实现 [Jestjs]【英文标题】:How to change mock implementation on a per single test basis [Jestjs] 【发布时间】:2018-07-25 05:55:36 【问题描述】:我想通过扩展默认模拟的行为更改模拟依赖项的实现,以每个单个测试为基础并在下一个测试执行时将其恢复为原始实现。
简而言之,这是我想要实现的目标:
-
模拟依赖
在单个测试中更改/扩展模拟实现
在下次测试执行时恢复原始模拟
我目前正在使用Jest v21
。
这是典型的 Jest 测试的样子:
__mocks__/myModule.js
const myMockedModule = jest.genMockFromModule('../myModule');
myMockedModule.a = jest.fn(() => true);
myMockedModule.b = jest.fn(() => true);
export default myMockedModule;
__tests__/myTest.js
import myMockedModule from '../myModule';
// Mock myModule
jest.mock('../myModule');
beforeEach(() =>
jest.clearAllMocks();
);
describe('MyTest', () =>
it('should test with default mock', () =>
myMockedModule.a(); // === true
myMockedModule.b(); // === true
);
it('should override myMockedModule.b mock result (and leave the other methods untouched)', () =>
// Extend change mock
myMockedModule.a(); // === true
myMockedModule.b(); // === 'overridden'
// Restore mock to original implementation with no side effects
);
it('should revert back to default myMockedModule mock', () =>
myMockedModule.a(); // === true
myMockedModule.b(); // === true
);
);
这是我迄今为止尝试过的:
1 - mockFn.mockImplementationOnce(fn)
专业人士
第一次调用后恢复到原始实现缺点
如果测试多次调用b
就会中断
在未调用 b
之前,它不会恢复到原始实现(在下一个测试中泄漏)
代码:
it('should override myModule.b mock result (and leave the other methods untouched)', () =>
myMockedModule.b.mockImplementationOnce(() => 'overridden');
myModule.a(); // === true
myModule.b(); // === 'overridden'
);
2 - jest.doMock(moduleName, factory, options)
专业人士
明确地重新模拟每个测试缺点
无法为所有测试定义默认模拟实现 无法扩展默认实现强制重新声明每个模拟 方法代码:
it('should override myModule.b mock result (and leave the other methods untouched)', () =>
jest.doMock('../myModule', () =>
return
a: jest.fn(() => true,
b: jest.fn(() => 'overridden',
);
myModule.a(); // === true
myModule.b(); // === 'overridden'
);
3 - 使用 setter 方法手动模拟(如 here 所述)
专业人士
完全控制模拟结果缺点
大量样板代码 难以长期维护代码:
__mocks__/myModule.js
const myMockedModule = jest.genMockFromModule('../myModule');
let a = true;
let b = true;
myMockedModule.a = jest.fn(() => a);
myMockedModule.b = jest.fn(() => b);
myMockedModule.__setA = (value) => a = value ;
myMockedModule.__setB = (value) => b = value ;
myMockedModule.__reset = () =>
a = true;
b = true;
;
export default myMockedModule;
__tests__/myTest.js
it('should override myModule.b mock result (and leave the other methods untouched)', () =>
myModule.__setB('overridden');
myModule.a(); // === true
myModule.b(); // === 'overridden'
myModule.__reset();
);
4 - jest.spyOn(object, methodName)
缺点
我无法将mockImplementation
恢复为原始模拟返回值,因此会影响接下来的测试
代码:
beforeEach(() =>
jest.clearAllMocks();
jest.restoreAllMocks();
);
// Mock myModule
jest.mock('../myModule');
it('should override myModule.b mock result (and leave the other methods untouched)', () =>
const spy = jest.spyOn(myMockedModule, 'b').mockImplementation(() => 'overridden');
myMockedModule.a(); // === true
myMockedModule.b(); // === 'overridden'
// How to get back to original mocked value?
);
【问题讨论】:
不错。但是你如何为像'@private-repo/module'这样的npm模块做选项2?我看到的大多数例子都有相对路径?这也适用于已安装的模块吗? 【参考方案1】:编写测试的一个很好的模式是创建一个设置工厂函数,它返回测试当前模块所需的数据。
下面是第二个示例之后的一些示例代码,尽管允许以可重用的方式提供默认值和覆盖值。
const spyReturns = returnValue => jest.fn(() => returnValue);
describe("scenario", () =>
beforeEach(() =>
jest.resetModules();
);
const setup = (mockOverrides) =>
const mockedFunctions =
a: spyReturns(true),
b: spyReturns(true),
...mockOverrides
jest.doMock('../myModule', () => mockedFunctions)
return
mockedModule: require('../myModule')
it("should return true for module a", () =>
const mockedModule = setup();
expect(mockedModule.a()).toEqual(true)
);
it("should return override for module a", () =>
const EXPECTED_VALUE = "override"
const mockedModule = setup( a: spyReturns(EXPECTED_VALUE));
expect(mockedModule.a()).toEqual(EXPECTED_VALUE)
);
);
重要的是,您必须重置已使用 jest.resetModules()
缓存的模块。这可以在beforeEach
或类似的拆解函数中完成。
有关更多信息,请参阅 jest 对象文档:https://jestjs.io/docs/jest-object。
【讨论】:
这实际上对我不起作用。在您的情况下,mockedModule
返回mockedModule: typeof jest
,其中.a()
是undefined
。而是返回advanceTimersByTime
、clearMocks
、resetAllMocks
等内容。
@ronnyrr,是的,调用jest.doMock()
后必须使用require(...)
获取mocked模块。此外,您必须在 beforeEach()
内调用 jest.resetModules()
,以便后续的 require(...)
调用使用模块的“最新”模拟版本。我已经提交了关于答案的编辑,以便在代码中也显示出来。【参考方案2】:
使用mockFn.mockImplementation(fn)。
import funcToMock from './somewhere';
jest.mock('./somewhere');
beforeEach(() =>
funcToMock.mockImplementation(() => /* default implementation */ );
// (funcToMock as jest.Mock)... in TS
);
test('case that needs a different implementation of funcToMock', () =>
funcToMock.mockImplementation(() => /* implementation specific to this test */ );
// (funcToMock as jest.Mock)... in TS
// ...
);
【讨论】:
这在模拟date-fns-tz
的 format
函数时对我有用。
这应该是公认的答案【参考方案3】:
在模拟单个方法时(当需要保持类/模块实现的其余部分完好无损时),我发现以下方法有助于重置单个测试中的任何实现调整。
我发现这种方法是最简洁的方法,不需要在文件开头使用jest.mock
等。您只需要在下面看到的代码来模拟MyClass.methodName
。另一个优点是默认情况下spyOn
保留原始方法实现,但也保存所有统计信息(调用次数、参数、结果等)以进行测试,并且在某些情况下必须保留默认实现。因此,您可以灵活地保留默认实现或通过简单添加 .mockImplementation
来更改它,如下面的代码中所述。
代码在 Typescript 中,cmets 突出显示了 JS 的差异(准确地说,差异在一行中)。使用 Jest 26.6 测试。
describe('test set', () =>
let mockedFn: jest.SpyInstance<void>; // void is the return value of the mocked function, change as necessary
// For plain JS use just: let mockedFn;
beforeEach(() =>
mockedFn = jest.spyOn(MyClass.prototype, 'methodName');
// Use the following instead if you need not to just spy but also to replace the default method implementation:
// mockedFn = jest.spyOn(MyClass.prototype, 'methodName').mockImplementation(() => /*custom implementation*/);
);
afterEach(() =>
// Reset to the original method implementation (non-mocked) and clear all the mock data
mockedFn.mockRestore();
);
it('does first thing', () =>
/* Test with the default mock implementation */
);
it('does second thing', () =>
mockedFn.mockImplementation(() => /*custom implementation just for this test*/);
/* Test utilising this custom mock implementation. It is reset after the test. */
);
it('does third thing', () =>
/* Another test with the default mock implementation */
);
);
【讨论】:
【参考方案4】:晚会有点晚,但如果其他人对此有疑问。
我们使用 TypeScript、ES6 和 babel 进行 react-native 开发。
我们通常在 __mocks__
根目录中模拟外部 NPM 模块。
我想在 aws-amplify 的 Auth 类中为特定测试覆盖模块的特定功能。
import Auth from 'aws-amplify';
import GetJwtToken from './GetJwtToken';
...
it('When idToken should return "123"', async () =>
const spy = jest.spyOn(Auth, 'currentSession').mockImplementation(() => (
getIdToken: () => (
getJwtToken: () => '123',
),
));
const result = await GetJwtToken();
expect(result).toBe('123');
spy.mockRestore();
);
要点: https://gist.github.com/thomashagstrom/e5bffe6c3e3acec592201b6892226af2
教程: https://medium.com/p/b4ac52a005d#19c5
【讨论】:
这是唯一对我有用的东西,样板量最少。在我的场景中,我在 TypeScript 中从一个没有默认导出的包中进行了命名导出,所以我最终使用了import * as MyModule;
,然后使用了const useQuery = MyModule
,所以我仍然可以以相同的方式使用导入,而无需在任何地方都使用MyModule.someExport
。跨度>
以上是关于如何在单个测试的基础上更改模拟实现 [Jestjs]的主要内容,如果未能解决你的问题,请参考以下文章
如何在 iOS 模拟器上使用某种语言键盘启动 Appium 测试
如何在 android 检测测试类中运行单个测试方法以及如何为此更改编辑配置