Jest Spy 未被调用
Jest Spy not being called
我正在尝试 运行 使用 winston 记录器包进行测试。我想监视 createlogger 函数并断言它是用正确的参数调用的。
Logger.test.ts
import { describe, expect, it, jest, beforeEach, afterEach } from '@jest/globals';
import { LogLevel } from 'api-specifications';
import winston, { format } from 'winston';
import { buildLogger } from './Logger';
import { LoggerConfig } from './Config';
describe('Logger', () => {
beforeEach(() => {
jest.spyOn(winston, 'createLogger');
});
afterEach(() => {
jest.restoreAllMocks();
});
it('should call winston createLogger with format.json when config.json is true', () => {
const config: LoggerConfig = {
json: true,
logLevel: LogLevel.INFO,
};
buildLogger(config);
expect(winston.createLogger).toHaveBeenCalledWith(
expect.objectContaining({
level: LogLevel.INFO,
format: format.json(),
}),
);
});
});
Logger.ts
import { createLogger, format, transports, Logger } from 'winston';
import { LoggerConfig } from './Config';
const logFormatter = format(info => {
const values = (info[Symbol.for('splat') as any as string] ?? [])
.filter(f => typeof f === 'object')
.reduce(
(acc, curr) => ({
...acc,
...curr,
}),
{},
);
const meta = Object.keys(values)
.map(k => ` - ${k}=${values[k]}`)
.join('');
return { ...info, [Symbol.for('message')]: `${info.level}: ${info.message}${meta}` };
});
export const buildLogger = (config: LoggerConfig): Logger =>
createLogger({
level: config.logLevel,
format: config.json ? format.json() : logFormatter(),
transports: [new transports.Console()],
});
然而,当我 运行 测试时,我得到以下输出
expect(jest.fn()).toHaveBeenCalledWith(...expected)
Expected: ObjectContaining {"format": {"options": {}}, "level": "info"}
Number of calls: 0
我不太确定发生了什么。
我正在使用以下版本的软件包:
- “开玩笑”:“28.1.0”
- “ts-jest”:“28.0.2”
尝试模拟:
jest.mock('winston', () => {
return {
createLogger: jest.fn()
}
});
describe('Logger', () => {
...
假设你使用的是ES模块,有很多方法可以解决这个问题。老实说,我不知道哪个更好(它们都有优点和缺点),甚至可能有一个 well-known 解决方案我还没有找到,但我对此表示怀疑。原因是,据我所读,Jest 对 ES 模块的支持仍然不完整,正如 documentation 指出的那样:
Please note that we currently don't support jest.mock
in a clean way in ESM, but that is something we intend to add proper support for in the future. Follow this issue for updates.
因此,以下所有内容只是变通方法,并非真正的解决方案。
#1 - 始终导入 default
对象
您可以通过两种方式导入 winston
:
import * as winston from 'winston'
:此符号 returns 一个 Module
对象,包含 export
ed 属性。其中可以找到一个default
属性,指向CommonJS模块的module.exports
import winston from 'winston'
:这是import { default as winston } from 'winston'
的语法糖。基本上,不是导入整个模块,而是得到 default
属性.
您可以阅读更多相关信息 here。
如果使用第一个导入符号,createLogger
可以通过两种方式访问:
[Module] object
{
...
createLogger: f() { ... }
default: {
...
createLogger: f() { ... }
}
}
我不确定模拟 Module
对象是否可行,但在您的情况下模拟 default.createLogger
就足够了。这很简单:
Logger.ts
import winston from 'winston'
export const buildLogger = async (config) => {
return winston.createLogger({
level: "info"
});
}
(Logger.test.ts是原来的。)
为什么这样做?因为 Logger.test.ts 和 Logger.ts 都分配给 winston
(引用) default
目的。 jest.spyOn(winston, 'createLogger')
在方法 default.createLogger
上创建了一个间谍,因为我们只导入了 default
对象。因此,模拟实现也与 Logger.ts 共享。
缺点是像 import { createLogger } from 'winston'
这样的导入语句无法工作,因为您访问的是 Module.createLogger
而不是 Module.default.createLogger
。
#2 - 首先模拟,然后导入
对于 ES 模块,import
语句被提升:即使您的 Logger.test.ts 的第一行是 jest.mock('winston', ...)
,Logger 模块将在该行之前加载(因为 import { buildLogger } from './Logger';
)。这意味着,在当前状态下,Logger.ts 引用了 createLogger
:
的实际实现
- 开玩笑加载Logger.test.ts
- 节点模块加载器加载所有使用
import ... from ...
导入的模块
- Logger.ts 被执行,前面加上
import { createLogger } from 'winston'
.
- Node继续执行Logger.test.ts,Jest在
createLogger
上创建了一个spy,但是Logger.ts 已经引用了该方法的实际实现。
为了避免提升,一种可能是使用动态导入:
Logger.test.ts
import { jest } from '@jest/globals';
jest.mock('winston', () => {
return {
// __esModule: true,
// default: () => "test",
createLogger: jest.fn()
}
});
const winston = await import('winston')
const { buildLogger } = await import('./Logger');
describe('Logger', () => {
it('should call winston createLogger with format.json when config.json is true', () => {
const config = {
json: true,
logLevel: "info",
};
buildLogger(config);
expect(winston.createLogger).toHaveBeenCalledWith(
expect.objectContaining({
level: "info"
}),
);
});
});
(Logger.ts是原来的。)
现在模块在导入记录器依赖项之前被模拟,这将看到 winston
的模拟版本。这里只是一些注意事项:
__esModule: true
在您的情况下可能不是必需的(或者我可能无法在没有动态导入的情况下正确模拟 ES 模块),但如果您必须模拟将在中使用的 ES 模块current 测试文件,那么你必须使用它。 (参见 here)
- 我必须使用
transform: {}
配置 Jest,请参阅 here
- 好处是实现代码保持不变,但测试代码的处理和维护变得更加复杂。此外,在某些情况下,这可能根本不起作用。
#3、#4...
至少还有另一种解决方案,但是,只看方法名称,我不会使用它:我说的是 unstable_mockModule
。我还没有找到它的官方文档,但它可能还没有准备好用于生产代码。
手动模拟可能是解决此问题的另一种方法,但我还没有尝试过。
老实说,我对这些解决方案中的任何一个都不完全满意。在这种情况下,我可能会使用第一个,以牺牲实现代码为代价,但我真的希望有人能找到更好的东西。
我正在尝试 运行 使用 winston 记录器包进行测试。我想监视 createlogger 函数并断言它是用正确的参数调用的。
Logger.test.ts
import { describe, expect, it, jest, beforeEach, afterEach } from '@jest/globals';
import { LogLevel } from 'api-specifications';
import winston, { format } from 'winston';
import { buildLogger } from './Logger';
import { LoggerConfig } from './Config';
describe('Logger', () => {
beforeEach(() => {
jest.spyOn(winston, 'createLogger');
});
afterEach(() => {
jest.restoreAllMocks();
});
it('should call winston createLogger with format.json when config.json is true', () => {
const config: LoggerConfig = {
json: true,
logLevel: LogLevel.INFO,
};
buildLogger(config);
expect(winston.createLogger).toHaveBeenCalledWith(
expect.objectContaining({
level: LogLevel.INFO,
format: format.json(),
}),
);
});
});
Logger.ts
import { createLogger, format, transports, Logger } from 'winston';
import { LoggerConfig } from './Config';
const logFormatter = format(info => {
const values = (info[Symbol.for('splat') as any as string] ?? [])
.filter(f => typeof f === 'object')
.reduce(
(acc, curr) => ({
...acc,
...curr,
}),
{},
);
const meta = Object.keys(values)
.map(k => ` - ${k}=${values[k]}`)
.join('');
return { ...info, [Symbol.for('message')]: `${info.level}: ${info.message}${meta}` };
});
export const buildLogger = (config: LoggerConfig): Logger =>
createLogger({
level: config.logLevel,
format: config.json ? format.json() : logFormatter(),
transports: [new transports.Console()],
});
然而,当我 运行 测试时,我得到以下输出
expect(jest.fn()).toHaveBeenCalledWith(...expected)
Expected: ObjectContaining {"format": {"options": {}}, "level": "info"}
Number of calls: 0
我不太确定发生了什么。 我正在使用以下版本的软件包:
- “开玩笑”:“28.1.0”
- “ts-jest”:“28.0.2”
尝试模拟:
jest.mock('winston', () => {
return {
createLogger: jest.fn()
}
});
describe('Logger', () => {
...
假设你使用的是ES模块,有很多方法可以解决这个问题。老实说,我不知道哪个更好(它们都有优点和缺点),甚至可能有一个 well-known 解决方案我还没有找到,但我对此表示怀疑。原因是,据我所读,Jest 对 ES 模块的支持仍然不完整,正如 documentation 指出的那样:
Please note that we currently don't support
jest.mock
in a clean way in ESM, but that is something we intend to add proper support for in the future. Follow this issue for updates.
因此,以下所有内容只是变通方法,并非真正的解决方案。
#1 - 始终导入 default
对象
您可以通过两种方式导入 winston
:
import * as winston from 'winston'
:此符号 returns 一个Module
对象,包含export
ed 属性。其中可以找到一个default
属性,指向CommonJS模块的module.exports
import winston from 'winston'
:这是import { default as winston } from 'winston'
的语法糖。基本上,不是导入整个模块,而是得到default
属性.
您可以阅读更多相关信息 here。
如果使用第一个导入符号,createLogger
可以通过两种方式访问:
[Module] object
{
...
createLogger: f() { ... }
default: {
...
createLogger: f() { ... }
}
}
我不确定模拟 Module
对象是否可行,但在您的情况下模拟 default.createLogger
就足够了。这很简单:
Logger.ts
import winston from 'winston'
export const buildLogger = async (config) => {
return winston.createLogger({
level: "info"
});
}
(Logger.test.ts是原来的。)
为什么这样做?因为 Logger.test.ts 和 Logger.ts 都分配给 winston
(引用) default
目的。 jest.spyOn(winston, 'createLogger')
在方法 default.createLogger
上创建了一个间谍,因为我们只导入了 default
对象。因此,模拟实现也与 Logger.ts 共享。
缺点是像 import { createLogger } from 'winston'
这样的导入语句无法工作,因为您访问的是 Module.createLogger
而不是 Module.default.createLogger
。
#2 - 首先模拟,然后导入
对于 ES 模块,import
语句被提升:即使您的 Logger.test.ts 的第一行是 jest.mock('winston', ...)
,Logger 模块将在该行之前加载(因为 import { buildLogger } from './Logger';
)。这意味着,在当前状态下,Logger.ts 引用了 createLogger
:
- 开玩笑加载Logger.test.ts
- 节点模块加载器加载所有使用
import ... from ...
导入的模块
- Logger.ts 被执行,前面加上
import { createLogger } from 'winston'
. - Node继续执行Logger.test.ts,Jest在
createLogger
上创建了一个spy,但是Logger.ts 已经引用了该方法的实际实现。
为了避免提升,一种可能是使用动态导入:
Logger.test.ts
import { jest } from '@jest/globals';
jest.mock('winston', () => {
return {
// __esModule: true,
// default: () => "test",
createLogger: jest.fn()
}
});
const winston = await import('winston')
const { buildLogger } = await import('./Logger');
describe('Logger', () => {
it('should call winston createLogger with format.json when config.json is true', () => {
const config = {
json: true,
logLevel: "info",
};
buildLogger(config);
expect(winston.createLogger).toHaveBeenCalledWith(
expect.objectContaining({
level: "info"
}),
);
});
});
(Logger.ts是原来的。)
现在模块在导入记录器依赖项之前被模拟,这将看到 winston
的模拟版本。这里只是一些注意事项:
__esModule: true
在您的情况下可能不是必需的(或者我可能无法在没有动态导入的情况下正确模拟 ES 模块),但如果您必须模拟将在中使用的 ES 模块current 测试文件,那么你必须使用它。 (参见 here)- 我必须使用
transform: {}
配置 Jest,请参阅 here - 好处是实现代码保持不变,但测试代码的处理和维护变得更加复杂。此外,在某些情况下,这可能根本不起作用。
#3、#4...
至少还有另一种解决方案,但是,只看方法名称,我不会使用它:我说的是 unstable_mockModule
。我还没有找到它的官方文档,但它可能还没有准备好用于生产代码。
手动模拟可能是解决此问题的另一种方法,但我还没有尝试过。
老实说,我对这些解决方案中的任何一个都不完全满意。在这种情况下,我可能会使用第一个,以牺牲实现代码为代价,但我真的希望有人能找到更好的东西。