如何在单个测试的基础上更改模拟实现 [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。而是返回advanceTimersByTimeclearMocksresetAllMocks 等内容。 @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-tzformat 函数时对我有用。 这应该是公认的答案【参考方案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 检测测试类中运行单个测试方法以及如何为此更改编辑配置

如何在模拟器上更改 Apple Watch 系统的文本大小?

maven ::在多模块项目中仅运行单个测试

使用 Typescript 从 Jest 手动模拟中导入函数