简体   繁体   English

如何使用 Jest 测试递归函数被调用 X 次? 如果我使用间谍方法,我的方法会永远挂起吗?

[英]How to test a recursive function is being called X amount of times using Jest? My method hangs forever if I use the spy method?

utils file实用程序文件

const isStatusError = (err: any): err is StatusError =>
  err.status !== undefined;

export const handleError = async (err: any, emailer?: Mailer) => {
  const sendErrorEmail = async (
    subject: string,
    text: string,
    emailer?: Mailer
  ) => {
    try {
      const mail: Pick<Mail, "from" | "to"> = {
        from: config.email.user,
        to: config.email.user,
      };

      // 2. This throws an error
      await emailer?.send({ ...mail, subject, text });
    } catch (err) {
      // 3. It should call this function recursively...
      await handleError(new EmailError(err), emailer);
    }
  };

  if (isStatusError(err)) {
    if (err instanceof ScrapeError) {
      console.log("Failed to scrape the website: \n", err.message);
    }

    if (err instanceof AgendaJobError) {
      console.log("Job ", err.message);
      // @TODO
    }

    if (err instanceof RepositoryError) {
      console.log("Repository: ");
      console.log(err.message);
      // @TODO
    }

    // 4. and eventually come here and end the test...
    if (err instanceof EmailError) {
      console.log("Failed to create email service", err);
    }

    // 1. It goes here first.
    if (err instanceof StatusError) {
      console.log("generic error", err);
      await sendErrorEmail("Error", "", emailer);
    }
  } else {
    if (err instanceof Error) {
      console.log("Generic error", err.message);
    }
    console.log("Generic error", err);
  }
};

test file测试文件

import * as utils from "./app.utils";
import { Mailer } from "./services/email/Emailer.types";
import { StatusError } from "./shared/errors";

const getMockEmailer = (implementation?: Partial<Mailer>) =>
  jest.fn<Mailer, []>(() => ({
    service: "gmail",
    port: 5432,
    secure: false,
    auth: {
      user: "user",
      pass: "pass",
    },
    verify: async () => true,
    send: async () => true,
    ...implementation,
  }))();

describe("error handling", () => {
  it("should handle email failed to send", async () => {
    const mockEmailer = getMockEmailer({
      send: async () => {
        throw new Error();
      },
    });

    // This line is the problem. If I comment it out, it's all good.
    const spiedHandleError = jest.spyOn(utils, "handleError");
    // @TODO: Typescript will complain mockEmailer is missing a private JS Class variable (e.g. #transporter) if you remove `as any`.
    await utils.handleError(new StatusError(500, ""), mockEmailer as any);

    expect(spiedHandleError).toBeCalledTimes(2);
  });
});

This test runs forever, and it is because I made handleError a spy function.这个测试永远运行,这是因为我把handleError了一个间谍函数。 I tried to import itself and run await utils.handleError(new EmailError(err), emailer) but it still continue to hang.我试图导入自己并运行await utils.handleError(new EmailError(err), emailer)但它仍然继续挂起。

So what happens is:那么会发生什么:

  1. It throws an Error.它抛出一个错误。
  2. It will then figure out it is a StatusError which is a custom error, and it will output the error and call a function to send an email.然后它会发现它是一个 StatusError,它是一个自定义错误,它会输出错误并调用一个函数来发送电子邮件。
  3. However, attempting to send an email throws another Error但是,尝试发送电子邮件会引发另一个错误
  4. It should then call itself with EmailError然后它应该用 EmailError 调用自己
  5. It will detect it is an EmailError and only output the error.它会检测到它是一个 EmailError 并且只输出错误。

Logic wise, there is no infinite loop.从逻辑上讲,没有无限循环。 In the utils file, if you comment this const spiedHandleError = jest.spyOn(utils, "handleError");utils文件中,如果你注释这个const spiedHandleError = jest.spyOn(utils, "handleError"); out, the test will be fine.出来了,考试就好了。

Is there a way around this somehow?有没有办法解决这个问题?

It's impossible to spy or mock a function that is used in the same module it was defined.不可能监视或模拟在定义它的同一模块中使用的函数。 This is the limitation of JavaScript, a variable cannot be reached from another scope.这是 JavaScript 的限制,不能从另一个作用域访问变量。 This is what happens:这是发生的事情:

let moduleObj = (() => {
  let foo = () => 'foo';
  let bar = () => foo();

  return { foo, bar };
})();

moduleObj.foo = () => 'fake foo';

moduleObj.foo() // 'fake foo'
moduleObj.bar() // 'foo'

The only way a function can be written to allow this defining and consistently using it as a method on some object like CommonJS exports:编写函数以允许定义并始终如一地将其用作某些对象(如 CommonJS 导出)的方法的唯一方法:

exports.handleError = async (...) => {
  ...
  exports.handleError(...);
  ...
};

This workaround is impractical and incompatible with ES modules.此解决方法不切实际且与 ES 模块不兼容。 Unless you do that, it's impossible to spy on recursively called function like handleError .除非你这样做,否则不可能监视像handleError这样递归调用的函数。 There's babel-plugin-rewire hack that allows to do this but it's known to be incompatible with Jest.babel-plugin-rewire hack 允许这样做,但已知它与 Jest 不兼容。

A proper testing strategy is to not assert that the function called itself (such assertions may be useful for debugging but nothing more) but assert effects that the recursion causes.正确的测试策略是不断言函数调用自身(此类断言可能对调试有用,但仅此而已),而是断言递归导致的效果。 In this case this includes console.log calls.在这种情况下,这包括console.log调用。

There are no reasons for spyOn to cause infinite loop. spyOn没有理由导致无限循环。 With no mock implementation provided, it's just a wrapper around original function.没有提供模拟实现,它只是原始函数的包装器。 And as explained above, there's no way how it can affect internal handleError calls, so it shouldn't affect the way tested function works.如上所述,它无法影响内部handleError调用,因此它不应该影响测试函数的工作方式。

It's unsafe to spy on utils ES module object because it's read-only by specification and can result in error depending on Jest setup.监视utils ES 模块对象是不安全的,因为它按规范是只读的,并且根据 Jest 设置可能会导致错误。

I realized it's my own logic that caused the infinite loop.我意识到是我自己的逻辑导致了无限循环。 I forgot to add the return statement to each of my if statement.我忘了将 return 语句添加到我的每个 if 语句中。

My spy function now works.我的间谍功能现在可以工作了。

    const spiedHandleError = jest.spyOn(utils, "handleError");
    await utils.handleError({
      err: new StatusError(500, "error"),
      emailer: mockEmailer,
    });

    expect(spiedHandleError).toBeCalledTimes(2);
    expect(spiedHandleError.mock.calls).toEqual([
      [{ err: new StatusError(500, "error"), emailer: mockEmailer }],
      [
        {
          err: new EmailError("failed to send an error report email."),
          emailer: mockEmailer,
        },
      ],
    ]);

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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