我在更改 Jest 中模拟模块的行为时遇到问题。我想模拟不同的行为来测试我的代码在这些不同情况下的行为方式。我不知道该怎么做,因为对
jest.mock()
的调用被提升到文件顶部,所以我不能只为每个测试调用 jest.mock()
。有没有一种方法可以更改一次测试的模拟模块的行为?
jest.mock('the-package-to-mock', () => ({
methodToMock: jest.fn(() => console.log('Hello'))
}));
import * as theThingToTest from './toTest'
import * as types from './types'
it('Test A', () => {
expect(theThingToTest.someAction().type).toBe(types.SOME_TYPE)
})
it('Test B', () => {
// I need the-package-to-mock.methodToMock to behave differently here
expect(theThingToTest.someAction().type).toBe(types.OTHER_TYPE)
})
在内部,正如您可以想象的那样,
theThingToTest.someAction()
导入并使用the-package-to-mock.methodToMock()
。
您可以使用间谍进行模拟并导入模拟的模块。在您的测试中,您可以使用
mockImplementation
: 设置模拟的行为方式
jest.mock('the-package-to-mock', () => ({
methodToMock: jest.fn()
}));
import { methodToMock } from 'the-package-to-mock'
it('test1', () => {
methodToMock.mockImplementation(() => 'someValue')
})
it('test2', () => {
methodToMock.mockImplementation(() => 'anotherValue')
})
我使用以下模式:
'use strict'
const packageToMock = require('../path')
jest.mock('../path')
jest.mock('../../../../../../lib/dmp.db')
beforeEach(() => {
packageToMock.methodToMock.mockReset()
})
describe('test suite', () => {
test('test1', () => {
packageToMock.methodToMock.mockResolvedValue('some value')
expect(theThingToTest.someAction().type).toBe(types.SOME_TYPE)
})
test('test2', () => {
packageToMock.methodToMock.mockResolvedValue('another value')
expect(theThingToTest.someAction().type).toBe(types.OTHER_TYPE)
})
})
说明:
您在测试套件级别模拟您尝试使用的类,确保在每次测试之前重置模拟,并且对于每个测试,您使用mockResolveValue来描述返回模拟时将返回的内容
另一种方法是使用 jest.doMock(moduleName,factory,options).
例如
the-package-to-mock.ts
:
export function methodToMock() {
return 'real type';
}
toTest.ts
:
import { methodToMock } from './the-package-to-mock';
export function someAction() {
return {
type: methodToMock(),
};
}
toTest.spec.ts
:
describe('45006254', () => {
beforeEach(() => {
jest.resetModules();
});
it('test1', () => {
jest.doMock('./the-package-to-mock', () => ({
methodToMock: jest.fn(() => 'type A'),
}));
const theThingToTest = require('./toTest');
expect(theThingToTest.someAction().type).toBe('type A');
});
it('test2', () => {
jest.doMock('./the-package-to-mock', () => ({
methodToMock: jest.fn(() => 'type B'),
}));
const theThingToTest = require('./toTest');
expect(theThingToTest.someAction().type).toBe('type B');
});
});
单元测试结果:
PASS examples/45006254/toTest.spec.ts
45006254
✓ test1 (2016 ms)
✓ test2 (1 ms)
-----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-----------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
toTest.ts | 100 | 100 | 100 | 100 |
-----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 3.443 s
源代码:https://github.com/mrdulin/jest-v26-codelab/tree/main/examples/45006254
spyOn
对我们来说效果最好。参见之前的回答:
在我的场景中,我尝试在
jest.mock
之外定义模拟函数,这将返回有关在定义变量之前尝试访问变量的错误。这是因为现代 Jest 会提升 jest.mock
,以便它可以在导入之前发生。不幸的是,这会让您的 const
和 let
无法按预期运行,因为代码提升到变量定义之上。有些人说使用 var
来代替,因为它会被提升,但大多数 linter 都会对你大喊大叫,为了避免这种黑客攻击,这就是我想出的:
这使我们能够处理像
new S3Client()
这样的情况,以便模拟所有新实例,同时也模拟实现。如果您愿意,您可能可以使用类似 jest-mock-extended
的内容来完全模拟实现,而不是显式定义模拟。
此示例将返回以下错误:
eferenceError: Cannot access 'getSignedUrlMock' before initialization
Test File
const sendMock = jest.fn()
const getSignedUrlMock = jest.fn().mockResolvedValue('signedUrl')
jest.mock('@aws-sdk/client-s3', () => {
return {
S3Client: jest.fn().mockImplementation(() => ({
send: sendMock.mockResolvedValue('file'),
})),
GetObjectCommand: jest.fn().mockImplementation(() => ({})),
}
})
jest.mock('@aws-sdk/s3-request-presigner', () => {
return {
getSignedUrl: getSignedUrlMock,
}
})
您必须在回调中推迟调用,如下所示:
getSignedUrl: jest.fn().mockImplementation(() => getSignedUrlMock())
我不想留下任何想象,虽然我从实际项目中删除了
some-s3-consumer
,但也不是太遥远。
Test File
import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3'
import { SomeS3Consumer } from './some-s3-consumer'
const sendMock = jest.fn()
const getSignedUrlMock = jest.fn().mockResolvedValue('signedUrl')
jest.mock('@aws-sdk/client-s3', () => {
return {
S3Client: jest.fn().mockImplementation(() => ({
send: sendMock.mockResolvedValue('file'),
})),
GetObjectCommand: jest.fn().mockImplementation(() => ({})),
}
})
jest.mock('@aws-sdk/s3-request-presigner', () => {
return {
// This is weird due to hoisting shenanigans
getSignedUrl: jest.fn().mockImplementation(() => getSignedUrlMock()),
}
})
describe('S3Service', () => {
const service = new SomeS3Consumer()
describe('S3 Client Configuration', () => {
it('creates a new S3Client with expected region and credentials', () => {
expect(S3Client).toHaveBeenCalledWith({
region: 'AWS_REGION',
credentials: {
accessKeyId: 'AWS_ACCESS_KEY_ID',
secretAccessKey: 'AWS_SECRET_ACCESS_KEY',
},
})
})
})
describe('#fileExists', () => {
describe('file exists', () => {
it('returns true', () => {
expect(service.fileExists('bucket', 'key')).resolves.toBe(true)
})
it('calls S3Client.send with GetObjectCommand', async () => {
await service.fileExists('bucket', 'key')
expect(GetObjectCommand).toHaveBeenCalledWith({
Bucket: 'bucket',
Key: 'key',
})
})
})
describe('file does not exist', () => {
beforeEach(() => {
sendMock.mockRejectedValue(new Error('file does not exist'))
})
afterAll(() => {
sendMock.mockResolvedValue('file')
})
it('returns false', async () => {
const response = await service.fileExists('bucket', 'key')
expect(response).toBe(false)
})
})
})
describe('#getSignedUrl', () => {
it('calls GetObjectCommand with correct bucket and key', async () => {
await service.getSignedUrl('bucket', 'key')
expect(GetObjectCommand).toHaveBeenCalledWith({
Bucket: 'bucket',
Key: 'key',
})
})
describe('file exists', () => {
it('returns the signed url', async () => {
const response = await service.getSignedUrl('bucket', 'key')
expect(response).toEqual(ok('signedUrl'))
})
})
describe('file does not exist', () => {
beforeEach(() => {
getSignedUrlMock.mockRejectedValue('file does not exist')
})
afterAll(() => {
sendMock.mockResolvedValue('file')
})
it('returns an S3ErrorGettingSignedUrl with expected error message', async () => {
const response = await service.getSignedUrl('bucket', 'key')
expect(response.val).toStrictEqual(new S3ErrorGettingSignedUrl('file does not exist'))
})
})
})
})
Andreas 的答案与函数配合得很好,这是我使用它得出的结论:
// You don't need to put import line after the mock.
import {supportWebGL2} from '../utils/supportWebGL';
// functions inside will be auto-mocked
jest.mock('../utils/supportWebGL');
const mocked_supportWebGL2 = supportWebGL2 as jest.MockedFunction<typeof supportWebGL2>;
// Make sure it return to default between tests.
beforeEach(() => {
// set the default
supportWebGL2.mockImplementation(() => true);
});
it('display help message if no webGL2 support', () => {
// only for one test
supportWebGL2.mockImplementation(() => false);
// ...
});
如果您的模拟模块不是函数,它将无法工作。我无法仅更改一次测试的导出布尔值的模拟:/。我的建议是,重构一个函数,或者制作另一个测试文件。
export const supportWebGL2 = /* () => */ !!window.WebGL2RenderingContext;
// This would give you: TypeError: mockImplementation is not a function
只是 jest.mock 默认值。 1.
jest.mock('./hooks/useCustom', () => jest.fn(() => value));
// or
jest.mock('./hooks/useCustom');
import useCustom from './hooks/useCustom'
it('test smt else', () => {
useCustom.mockReturnValueOnce(differentValue)
// with ts: (useCustom as any).mockReturnValueOnce(differentValue)
// render hook or component
// expect(...)
})
import React from 'react';
import Component from 'component-to-mock';
jest.mock('component-to-mock', () => jest.fn());
describe('Sample test', () => {
it('first test', () => {
Component.mockImplementation(({ props }) => <div>first mock</div>);
});
it('second test', () => {
Component.mockImplementation(({ props }) => <div>second mock</div>);
});
});