嘲笑没有装载机的ESM
#javascript #node #测试

我已经越来越多地使用了新的基于node:test的测试框架,最近遇到了一个棘手的问题。我经常模拟文件的依赖项,以使测试集中在要测试的单元上,而不是整个应用程序。但是,我对使用node:test进行的每项研究都意味着,现在不可能嘲笑ESM进口,并且不会在不弄乱装载机的情况下可以预见的将来。

提出的问题和讨论指出使用testdouble.jsesmock等工具。我想看看是否可以避免它们,然后再添加需要配置的新内容。

我探索了一些不需要装载器的依赖项注入选项,其中大多数库也以错误的方式摩擦了我。倾向于包含许多样板代码,并且需要特定类型的表达式,例如服务类。同样,我想看看我是否可以保持更简单。

RunJS进行了一些探索后,我想出了一种我很满意的模式。

首先,一个例子

这是我在做什么的快速示例。

users.js

export async function find(id) {
  return db.query("SELECT * FROM users WHERE id = $1", [id]);
}

get-user.js

import { find } from "./users.js";

export async function handleRequest(request, response) {
  const user = await find(request.params.id);

  response.status(200).send({ user });
}

get-user.test.js

import { test } from "node:test";
import { assert } from "node:assert/strict";
import { handleRequest } from "./get-user.js";

test("success", async () => {
  const resp = await handleRequest(fakeRequest("/users/12"));
  assert.equal(resp.statusCode, 200);
});

如果我要运行上述测试,则​​在调用find函数时,查询将击中数据库。我不希望发生这种情况,因为它要求我们正确设置数据库,并且在一个可能需要大量时间的大型测试套件中。

顺便说一句,我的一般经验法则是我在测试数据层时测试数据库,但是一旦我移出数据层,我倾向于模拟数据库,而不是将所有类型的数据推入数据库。这往往更容易,并且可以使我的测试更快地运行。

标记可模拟的依赖项

首先,让我们更新我们要测试的单元,以标记要模拟的数据。我们不知道这将最终如何起作用,但是我们可以想象一个高阶功能可以处理我们嘲笑的能力:

get-user.js

import { find } from "./users.js";
import { mockable } from "./mockable.js";

export const findUser = mockable(find);

export async function handleRequest(request, response) {
  const user = await findUser(request.params.id);

  response.status(200).send({ user });
}

我们在这里更改的所有更改都添加了一个不存在的函数,称为mockable,我们将find函数包装在其中。我们导出了它,因为我们知道我们需要在测试中访问它。

没有生产替代

由于还没有mockable.js,因此上面的代码将失败,所以让我们编写它。首先,我们知道在生产中我们希望它返回原始功能。蛋糕!

mockable.js

export function mockable(fn) {
  if (process.env.NODE_ENV === "production") {
    return fn;
  }
}

除了生产之外,这在任何地方都无法工作,但是我们知道它在生产中增加了有限的开销,因为它确实返回了原始功能。

允许覆盖

现在,让我们构建一个模拟的替代案例。我们希望模拟能够返回可呼叫功能,但是该功能应该能够超过。这是这样的外观:

mockable.js

export function mockable(fn) {
  if (process.env.NODE_ENV === "production") {
    return fn;
  }

  // impl holds the overridden implementation of the function, if it is
  // overridden
  const impl = undefined;

  // call impl or the original function, based on if impl is set
  const wrap = function (...args) {
    if (impl) {
      return impl(...args);
    } else {
      return fn(...args);
    }
  };

  // attach an override function to wrap
  wrap.override = function (fn) {
    impl = fn;
    return fn;
  };

  // clear the override
  wrap.clear = function () {
    impl = undefined;
  };

  return wrap;
}

在上面的代码中,我们倾向于关闭,并且JavaScript函数是对象的想法,因此您可以将函数设置为函数的属性!这很奇怪,但在这里也很有用。

因此,我们的mockable函数始终返回一个函数。在生产中,它仅返回原始功能。不在生产中返回一个功能,该函数还包括可呼叫的overrideclear属性。

在测试中使用模拟

现在是时候更新测试以依靠新的mockable实现了:

get-user.test.js

import { test } from "node:test";
import { assert } from "node:assert/strict";
import { handleRequest, findUser } from "./get-user.js";

test("success", async (t) => {
  // override findUser for this test
  findUser.override((id) => {
    return {
      id,
      name: "Test Tester",
      email: "test@test.com",
      createdAt: new Date(),
      updatedAt: new Date(),
    };
  });

  // clear the override after the test runs
  t.after(() => findUser.clear());

  const resp = await handleRequest(fakeRequest("/users/12"));
  assert.equal(resp.statusCode, 200);
});

完毕!

就是这样!我们有一种相当紧凑的方法来使用高阶功能在测试中覆盖功能,而不需要任何形式的装载器。

这不涵盖诸如课堂上的方法之类的东西,但这主要是一个类似的想法,实现略有不同。