[英]Jest Spy not being called
I am trying to run a test using the winston logger package. I want to spy on the createlogger function and assert that it is being called with the correct argument.我正在尝试使用winston记录器 package 运行测试。我想监视 createlogger function 并断言它是用正确的参数调用的。
Logger.test.ts记录器.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记录器.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()],
});
However when i run the test i get the following output但是,当我运行测试时,我得到以下 output
expect(jest.fn()).toHaveBeenCalledWith(...expected)
Expected: ObjectContaining {"format": {"options": {}}, "level": "info"}
Number of calls: 0
Im not quite sure what going on.我不太确定发生了什么。 Im using the following versions of packages:
我使用以下版本的软件包:
Assuming you are using ES modules, there are many ways to solve this issue.假设你使用的是 ES 模块,有很多方法可以解决这个问题。 Honestly, I do not know which one is better (all of them have upsides and downsides), there might even be a well-known solution that I have not found yet, but I doubt it.
老实说,我不知道哪个更好(它们都有优点和缺点),甚至可能有一个我还没有找到的知名解决方案,但我对此表示怀疑。 The reason is that, from what I have read, Jest support for ES modules is still incomplete, as the documentation points out:
原因是,据我所读,Jest 对 ES 模块的支持仍然不完整,正如文档所指出的:
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.请注意,我们目前在 ESM 中不以干净的方式支持
jest.mock
,但我们打算在未来添加适当的支持。 Follow this issue for updates.请关注此问题以获取更新。
So, all the followings are just workarounds, not real solutions.因此,以下所有内容都只是变通方法,而不是真正的解决方案。
default
object default
object You can import winston
in 2 ways:您可以通过两种方式导入
winston
:
import * as winston from 'winston'
: this notation returns a Module
object, containing the export
ed properties. import * as winston from 'winston'
:此表示法返回一个Module
object,其中包含export
ed 属性。 Among them you can find a default
property, pointing to module.exports
of the CommonJS module.default
属性,指向CommonJS模块的module.exports
。import winston from 'winston'
: this is a syntactic sugar for import { default as winston } from 'winston'
. import winston from 'winston'
:这是import { default as winston } from 'winston'
的语法糖。 Basically, instead of importing the entire module, you just get the default
property.default
属性即可。 You can read more about it here .您可以在此处阅读更多相关信息。
createLogger
can be accessed in 2 ways if you use the first import notation:如果您使用第一个导入符号,则可以通过两种方式访问
createLogger
:
[Module] object
{
...
createLogger: f() { ... }
default: {
...
createLogger: f() { ... }
}
}
I am not sure mocking a Module
object is possible, but in your case it is enough to mock default.createLogger
.我不确定 mocking
Module
object 是否可行,但在您的情况下模拟default.createLogger
就足够了。 This is quite easy:这很容易:
Logger.ts记录器.ts
import winston from 'winston'
export const buildLogger = async (config) => {
return winston.createLogger({
level: "info"
});
}
( Logger.test.ts is the original one.) ( Logger.test.ts是原始的。)
Why does this work?为什么这行得通? Because both Logger.test.ts and Logger.ts assign to
winston
(a reference to) the default
object. jest.spyOn(winston, 'createLogger')
creates a spy on the method default.createLogger
, because we have imported only the default
object. Therefore, the mocked implementation gets shared with Logger.ts as well.因为Logger.test.ts和Logger.ts都将
default
object 分配给winston
(引用) jest.spyOn(winston, 'createLogger')
在方法default.createLogger
上创建了一个间谍,因为我们只导入了default
object。因此,模拟实现也与Logger.ts共享。
The downside is that an import statement like import { createLogger } from 'winston'
cannot work because you are accessing Module.createLogger
instead of Module.default.createLogger
.缺点是像
import { createLogger } from 'winston'
这样的导入语句无法工作,因为您正在访问Module.createLogger
而不是Module.default.createLogger
。
With ES modules, import
statements are hoisted: even if the first line of your Logger.test.ts was jest.mock('winston', ...)
, the Logger module would be loaded before that line (because of import { buildLogger } from './Logger';
).对于 ES 模块,
import
语句会被提升:即使你的Logger.test.ts的第一行是jest.mock('winston', ...)
, Logger模块也会在那行之前加载(因为import { buildLogger } from './Logger';
)。 This means that, in the current state, Logger.ts references the actual implementation of createLogger
:这意味着,在当前 state 中, Logger.ts引用了
createLogger
的实际实现:
import... from...
import... from...
的模块import { createLogger } from 'winston'
. import { createLogger } from 'winston'
。createLogger
, but Logger.ts already references the actual implementation of that method. createLogger
引用了该方法的实际实现。 To avoid the hoisting, a possibility is to use dynamic imports:为了避免提升,一种可能是使用动态导入:
Logger.test.ts记录器.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 is the original one.) ( Logger.ts是原始的。)
Now the module is mocked before importing the logger dependency, which will see the mocked version of winston
.现在模块在导入记录器依赖项之前被模拟,这将看到
winston
的模拟版本。 Just a few notes here:这里只是一些注意事项:
__esModule: true
is probably not necessary in your case (or maybe I was not able to correctly mock the ES module without dynamic imports), but in case you have to mock an ES module that you will use in the current test file, then you have to use it. __esModule: true
在您的情况下可能不是必需的(或者我可能无法在没有动态导入的情况下正确模拟 ES 模块),但是如果您必须模拟将在当前测试文件中使用的 ES 模块,那么您必须使用它。 (See here ) transform: {}
, see heretransform: {}
配置 Jest,请看这里 There is at least another solution out there, but, just looking at the method name, I would not use it: I am talking about unstable_mockModule
.至少还有另一种解决方案,但是,只看方法名称,我不会使用它:我说的是
unstable_mockModule
。 I have not found official documentation for it, but it is probably not ready for production code.我还没有找到它的官方文档,但它可能还没有准备好用于生产代码。
Manual mocks could be another way to solve this, but I have not tried it.手动模拟可能是解决此问题的另一种方法,但我还没有尝试过。
Honestly, I am not fully satisfied with any of these solutions.老实说,我对这些解决方案中的任何一个都不完全满意。 In this case, I would probably use the first one, at the expense of the implementation code, but I really hope someone finds something better.
在这种情况下,我可能会使用第一个,以牺牲实现代码为代价,但我真的希望有人能找到更好的东西。
Try mocking:尝试 mocking:
jest.mock('winston', () => {
return {
createLogger: jest.fn()
}
});
describe('Logger', () => {
...
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.