简体   繁体   English

Jest Spy 未被调用

[英]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:我使用以下版本的软件包:

  • "jest": "28.1.0" “开玩笑”:“28.1.0”
  • "ts-jest": "28.0.2" “ts-笑话”:“28.0.2”

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.因此,以下所有内容都只是变通方法,而不是真正的解决方案。


#1 - Always import the default object #1 - 始终导入default object

You can import winston in 2 ways:您可以通过两种方式导入winston

  1. 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
  2. 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.tsLogger.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


#2 - First mock, then import #2 - 首先模拟,然后导入

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的实际实现:

  1. Jest loads Logger.test.ts Jest 加载Logger.test.ts
  2. Node module loader loads all the modules imported with import... from... Node 模块加载器加载所有使用import... from...的模块
  3. Logger.ts is executed, preceded by with import { createLogger } from 'winston' . Logger.ts被执行,前面是 with import { createLogger } from 'winston'
  4. Node continues to execute Logger.test.ts , Jest creates a spy on createLogger , but Logger.ts already references the actual implementation of that method. Node 继续执行Logger.test.ts ,Jest 在createLogger 上创建了一个间谍,但 Logger.ts 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 ) (见这里
  • I had to configure Jest with transform: {} , see here我必须使用transform: {}配置 Jest,请看这里
  • The upside is that the implementation code stays unchanged, but the test code becomes more complex to handle and maintain.好处是实现代码保持不变,但测试代码的处理和维护变得更加复杂。 Besides, there could be some situations where this does not work at all.此外,在某些情况下,这可能根本不起作用。

#3, #4... #3,#4……

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM