我已经越来越多地使用了新的基于node:test
的测试框架,最近遇到了一个棘手的问题。我经常模拟文件的依赖项,以使测试集中在要测试的单元上,而不是整个应用程序。但是,我对使用node:test
进行的每项研究都意味着,现在不可能嘲笑ESM进口,并且不会在不弄乱装载机的情况下可以预见的将来。
提出的问题和讨论指出使用testdouble.js和esmock等工具。我想看看是否可以避免它们,然后再添加需要配置的新内容。
我探索了一些不需要装载器的依赖项注入选项,其中大多数库也以错误的方式摩擦了我。倾向于包含许多样板代码,并且需要特定类型的表达式,例如服务类。同样,我想看看我是否可以保持更简单。
在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
函数始终返回一个函数。在生产中,它仅返回原始功能。不在生产中返回一个功能,该函数还包括可呼叫的override
和clear
属性。
在测试中使用模拟
现在是时候更新测试以依靠新的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);
});
完毕!
就是这样!我们有一种相当紧凑的方法来使用高阶功能在测试中覆盖功能,而不需要任何形式的装载器。
这不涵盖诸如课堂上的方法之类的东西,但这主要是一个类似的想法,实现略有不同。