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

时间:2018-02-14 15:38:44

标签: javascript unit-testing mocking jestjs

我想通过扩展默认模拟单一测试基础更改模拟依赖项的实现当以下测试执行时,#39;行为和将其恢复到原始实现。

更简单地说,这就是我想要实现的目标:

  1. 模拟依赖
  2. 在单个测试中更改/扩展模拟实现
  3. 在下次测试执行时将恢复为原始模拟
  4. 我目前正在使用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 eeffects
      });
    
      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 the original mocked value?
    });
    

    提前感谢您提出任何意见或建议!

4 个答案:

答案 0 :(得分:12)

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

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

const spyReturns = returnValue => jest.fn(() => returnValue);

describe("scenario", () => {
  const setup = (mockOverrides) => {
    const mockedFunctions =  {
      a: spyReturns(true),
      b: spyReturns(true),
      ...mockOverrides
    }
    return {
      mockedModule: jest.doMock('../myModule', () => mockedFunctions);
    }
  }

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

答案 1 :(得分:3)

聚会晚一点,但是如果有人对此有疑问。

我们使用TypeScript,ES6和babel进行本机开发。

我们通常在__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

答案 2 :(得分:2)

使用mockFn.mockImplementation(fn)

将默认实现放入beforeEach中。每次测试之前,模拟都会重置为此。

要覆盖,请在测试中使用mockImplementation

这将覆盖测试中任何/所有调用的模拟行为,并且在下一次测试之前将被beforeEach实现覆盖。

例如:

import { funcToMock } from './somewhere';
jest.mock('./somewhere');

beforeEach(() => {
  funcToMock.mockImplementation(() => { /* default implementation */ });
});

test('case that needs a different implementation of funcToMock', () => {
  funcToMock.mockImplementation(() => { /* implementation specific to this test */ });
  // ...
});

答案 3 :(得分:0)

在模拟单个方法时(需要保持类/模块实现的其余部分完整无缺),我发现以下方法有助于重置单个测试中的任何实现调整。

我发现这种方法是最简洁的方法,无需在文件开头 jest.mock 等。您只需要在下面看到的代码来模拟 MyClass.methodName。另一个优点是默认情况下 spyOn 保留原始方法实现,但也保存所有统计信息(调用数量、参数、结果等)以进行测试,并且在某些情况下必须保留默认实现。因此,您可以灵活地保留默认实现或通过简单添加 .mockImplementation 来更改它,如下面的代码中所述。

代码在 Typescript 中,注释突出了 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 */
    });
});