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

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

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

  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. < p > 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. < p > 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. < p > 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恢复到原始的模拟返回值,因此会影响接下来的测试
143037 次浏览

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

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


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或类似的teardown函数中完成。

有关更多信息,请参阅笑话对象文档:https://jestjs.io/docs/jest-object

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


// ...
});

有点晚了,但如果有人对此有意见。

我们使用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();
});
< p >要点: https://gist.github.com/thomashagstrom/e5bffe6c3e3acec592201b6892226af2 < / p > < p >教程: https://medium.com/p/b4ac52a005d#19c5 < / p >

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

我发现这种方法是最简洁的,不需要在文件开头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 */
});
});

我没有设法在测试本身中定义模拟,所以我发现我可以模拟相同服务模拟的几个结果,如下所示:

jest.mock("@/services/ApiService", () => {
return {
apiService: {
get: jest.fn()
.mockResolvedValueOnce({response: {value:"Value", label:"Test"}})
.mockResolvedValueOnce(null),
}
};
});

我希望它能帮助到一些人:)

这是我在这个博客上发现的一个很酷的方法https://mikeborozdin.com/post/changing-jest-mocks-between-tests/

import { sayHello } from './say-hello';
import * as config from './config';


jest.mock('./config', () => ({
__esModule: true,
CAPITALIZE: null
}));


describe('say-hello', () => {
test('Capitalizes name if config requires that', () => {
config.CAPITALIZE = true;


expect(sayHello('john')).toBe('Hi, John');
});


test('does not capitalize name if config does not require that', () => {
config.CAPITALIZE = false;


expect(sayHello('john')).toBe('Hi, john');
});
});