如何在每个测试的基础上更改模拟实现?

2022-08-30 01:24:40

我想通过扩展默认模拟的行为并在执行下一个测试时将其恢复为原始实现,在每个测试的基础上更改模拟依赖项的实现。

更簡單地說,這是我試圖實現的:

  1. 模拟依赖关系
  2. 在单个测试中更改/扩展模拟实现
  3. 执行下一个测试时恢复为原始模拟

我目前正在使用Jest v21。以下是典型测试的样子:

// __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)

    it('should override myModule.b mock result (and leave the other methods untouched)', () => {
    
      myMockedModule.b.mockImplementationOnce(() => 'overridden');
    
      myModule.a(); // === true
      myModule.b(); // === 'overridden'
    });
    

    优点

    • 首次调用后恢复到原始实现

    缺点

    • 如果测试多次调用,它将中断b
    • 它不会恢复到原始实现,直到不被调用(在下一个测试中泄漏)b
  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 方法进行手动模拟(如此所述)

    // __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)

    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?
    });
    

    缺点

    • 我无法恢复到原始模拟返回值,因此会影响下一个测试mockImplementation

答案 1

使用 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

  // ...
});

答案 2

编写测试的一个很好的模式是创建一个设置工厂函数,该函数返回测试当前模块所需的数据。

下面是第二个示例后面的一些示例代码,尽管允许以可重用的方式提供默认值和覆盖值。


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