在本页
在 Deno 中进行测试
Deno 有一个内置的测试运行器,你可以用它来测试 JavaScript 或 TypeScript 代码。
快速入门 跳转到标题
首先,让我们创建一个名为 url_test.ts
的文件,并使用 Deno.test()
函数注册一个测试用例。
// url_test.ts
import { assertEquals } from "https://deno.land/[email protected]/assert/mod.ts";
Deno.test("url test", () => {
const url = new URL("./foo.js", "https://deno.land/");
assertEquals(url.href, "https://deno.land/foo.js");
});
其次,使用 deno test
子命令运行测试。
$ deno test url_test.ts
running 1 test from file:///dev/url_test.js
test url test ... ok (2ms)
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out (9ms)
编写测试 跳转到标题
要定义一个测试,你需要使用 Deno.test
API 注册它。这个 API 有多个重载,以允许最大的灵活性,并可以轻松地在这些形式之间切换(例如,当你需要快速专注于一个测试进行调试时,使用 only: true
选项)。
import { assertEquals } from "https://deno.land/[email protected]/assert/mod.ts";
// Compact form: name and function
Deno.test("hello world #1", () => {
const x = 1 + 2;
assertEquals(x, 3);
});
// Compact form: named function.
Deno.test(function helloWorld3() {
const x = 1 + 2;
assertEquals(x, 3);
});
// Longer form: test definition.
Deno.test({
name: "hello world #2",
fn: () => {
const x = 1 + 2;
assertEquals(x, 3);
},
});
// Similar to compact form, with additional configuration as a second argument.
Deno.test("hello world #4", { permissions: { read: true } }, () => {
const x = 1 + 2;
assertEquals(x, 3);
});
// Similar to longer form, with test function as a second argument.
Deno.test(
{ name: "hello world #5", permissions: { read: true } },
() => {
const x = 1 + 2;
assertEquals(x, 3);
},
);
// Similar to longer form, with a named test function as a second argument.
Deno.test({ permissions: { read: true } }, function helloWorld6() {
const x = 1 + 2;
assertEquals(x, 3);
});
异步函数 跳转到标题
你也可以通过传递一个返回 Promise 的测试函数来测试异步代码。为此,你可以在定义函数时使用 async
关键字。
import { delay } from "https://deno.land/[email protected]/async/delay.ts";
Deno.test("async hello world", async () => {
const x = 1 + 2;
// await some async task
await delay(100);
if (x !== 3) {
throw Error("x should be equal to 3");
}
});
测试步骤 跳转到标题
测试步骤 API 提供了一种方法来报告测试中的不同步骤,并在该测试中执行设置和拆卸代码。
import { assertEquals } from "https://deno.land/[email protected]/assert/mod.ts";
import { Client } from "https://deno.land/x/[email protected]/mod.ts";
interface User {
id: number;
name: string;
}
interface Book {
id: number;
title: string;
}
Deno.test("database", async (t) => {
const client = new Client({
user: "user",
database: "test",
hostname: "localhost",
port: 5432,
});
await client.connect();
// provide a step name and function
await t.step("insert user", async () => {
const users = await client.queryObject<User>(
"INSERT INTO users (name) VALUES ('Deno') RETURNING *",
);
assertEquals(users.rows.length, 1);
assertEquals(users.rows[0].name, "Deno");
});
// or provide a test definition
await t.step({
name: "insert book",
fn: async () => {
const books = await client.queryObject<Book>(
"INSERT INTO books (name) VALUES ('The Deno Manual') RETURNING *",
);
assertEquals(books.rows.length, 1);
assertEquals(books.rows[0].title, "The Deno Manual");
},
ignore: false,
// these default to the parent test or step's value
sanitizeOps: true,
sanitizeResources: true,
sanitizeExit: true,
});
// nested steps are also supported
await t.step("update and delete", async (t) => {
await t.step("update", () => {
// even though this test throws, the outer promise does not reject
// and the next test step will run
throw new Error("Fail.");
});
await t.step("delete", () => {
// ...etc...
});
});
// steps return a value saying if they ran or not
const testRan = await t.step({
name: "copy books",
fn: () => {
// ...etc...
},
ignore: true, // was ignored, so will return `false`
});
// steps can be run concurrently if sanitizers are disabled on sibling steps
const testCases = [1, 2, 3];
await Promise.all(testCases.map((testCase) =>
t.step({
name: `case ${testCase}`,
fn: async () => {
// ...etc...
},
sanitizeOps: false,
sanitizeResources: false,
sanitizeExit: false,
})
));
client.end();
});
输出
test database ...
test insert user ... ok (2ms)
test insert book ... ok (14ms)
test update and delete ...
test update ... FAILED (17ms)
Error: Fail.
at <stack trace omitted>
test delete ... ok (19ms)
FAILED (46ms)
test copy books ... ignored (0ms)
test case 1 ... ok (14ms)
test case 2 ... ok (14ms)
test case 3 ... ok (14ms)
FAILED (111ms)
注意
- 测试步骤 **必须在父测试/步骤函数解析之前等待**,否则你会得到运行时错误。
- 测试步骤不能并发运行,除非兄弟步骤或父测试上的消毒器被禁用。
- 如果嵌套步骤,请确保为父步骤指定一个参数。
Deno.test("my test", async (t) => { await t.step("step", async (t) => { // note the `t` used here is for the parent step and not the outer `Deno.test` await t.step("sub-step", () => { }); }); });
运行测试 跳转到标题
要运行测试,请使用包含测试函数的文件调用 deno test
。你也可以省略文件名,在这种情况下,当前目录(递归)中所有匹配 glob {*_,*.,}test.{ts, tsx, mts, js, mjs, jsx}
的测试都将被运行。如果你传递一个目录,则该目录中所有匹配此 glob 的文件都将被运行。
glob 展开为
- 名为
test.{ts, tsx, mts, js, mjs, jsx}
的文件, - 或以
.test.{ts, tsx, mts, js, mjs, jsx}
结尾的文件, - 或以
_test.{ts, tsx, mts, js, mjs, jsx}
结尾的文件。
# Run all tests in the current directory and all sub-directories
deno test
# Run all tests in the util directory
deno test util/
# Run just my_test.ts
deno test my_test.ts
# Run test modules in parallel
deno test --parallel
请注意,从 Deno v1.24 开始,一些测试选项可以通过 配置文件 进行配置。
⚠️ 如果你想将额外的 CLI 参数传递给测试文件,请使用
--
来通知 Deno 剩余的参数是脚本参数。
# Pass additional arguments to the test file
deno test my_test.ts -- -e --foo --bar
deno test
使用与 deno run
相同的权限模型,因此需要例如 --allow-write
才能在测试期间写入文件系统。
要查看 deno test
的所有运行时选项,你可以参考命令行帮助。
deno help test
过滤 跳转到标题
有很多选项可以过滤你正在运行的测试。
命令行过滤 跳转到标题
可以使用命令行 --filter
选项单独或成组运行测试。
过滤器标志接受字符串或模式作为值。
假设以下测试
Deno.test({ name: "my-test", fn: myTest });
Deno.test({ name: "test-1", fn: test1 });
Deno.test({ name: "test-2", fn: test2 });
此命令将运行所有这些测试,因为它们都包含“test”一词。
deno test --filter "test" tests/
另一方面,以下命令使用模式,并将运行第二个和第三个测试。
deno test --filter "/test-*\d/" tests/
要让 Deno 知道您要使用模式,请将您的过滤器用正斜杠括起来,就像 JavaScript 语法糖用于正则表达式一样。
在配置文件中包含和排除路径 跳转到标题
您还可以通过在 Deno 配置文件中指定要包含或排除的路径来过滤测试。
例如,如果您只想测试 src/fetch_test.ts
和 src/signal_test.ts
并排除 out/
中的所有内容
{
"test": {
"include": [
"src/fetch_test.ts",
"src/signal_test.ts"
]
}
}
或者更有可能
{
"test": {
"exclude": ["out/"]
}
}
然后在与配置文件相同的目录树中运行 deno test
将考虑这些选项。
测试定义过滤 跳转到标题
在测试本身中,您有两个过滤选项。
过滤掉(忽略这些测试) 跳转到标题
有时您想根据某种条件忽略测试(例如,您只想在 Windows 上运行测试)。为此,您可以在测试定义中使用 ignore
布尔值。如果将其设置为 true,则测试将被跳过。
Deno.test({
name: "do macOS feature",
ignore: Deno.build.os !== "darwin",
fn() {
// do MacOS feature here
},
});
过滤进(只运行这些测试) 跳转到标题
有时您可能在一个大型测试类中遇到问题,并且希望只关注该测试并暂时忽略其他测试。为此,您可以使用 only
选项来告诉测试框架只运行将此选项设置为 true 的测试。多个测试可以设置此选项。虽然测试运行将报告每个测试的成功或失败,但如果任何测试被标记为 only
,则整个测试运行将始终失败,因为这只是一个临时措施,它禁用了几乎所有测试。
Deno.test({
name: "Focus on this test only",
only: true,
fn() {
// test complicated stuff here
},
});
快速失败 跳转到标题
如果您有一个长时间运行的测试套件,并且希望它在第一次失败时停止,您可以在运行套件时指定 --fail-fast
标志。
deno test --fail-fast
报告器 跳转到标题
Deno 附带三个内置报告器
pretty
(默认)点
junit
您可以使用 --reporter
标志指定要使用的报告器。
# use default pretty reporter
$ deno test
# use dot reporter with concise output
$ deno test --reporter=dot
# use JUnit reporter
$ deno test --reporter=junit
您还可以将机器可读的 JUnit 报告的输出写入文件,同时仍然享受终端中的人类可读输出。在这种情况下,请指定 --junit-path
标志
$ deno test --junit-path=./report.xml
示例:使用 Sinon 监视函数 跳转到标题
测试间谍是函数替身,用于断言函数的内部行为是否符合预期。Sinon 是一个广泛使用的测试库,它提供测试间谍,可以通过从 NPM 导入它来在 Deno 中使用
import sinon from "npm:sinon";
假设我们有两个函数 foo
和 bar
,并且想要断言 bar
在 foo
执行期间被调用。使用 Sinon 可以通过几种方式实现这一点,一种方法是让函数 foo
接受另一个函数作为参数
// my_file.js
export function bar() {/*...*/}
export function foo(fn) {
fn();
}
这样,我们可以在应用程序代码中调用 foo(bar)
,或者在测试代码中将间谍函数包装在 bar
周围并调用 foo(spy)
import sinon from "npm:sinon";
import { assertEquals } from "https://deno.land/[email protected]/assert/mod.ts";
import { bar, foo } from "./my_file.js";
Deno.test("calls bar during execution of foo", () => {
// create a test spy that wraps 'bar'
const spy = sinon.spy(bar);
// call function 'foo' and pass the spy as an argument
foo(spy);
assertEquals(spy.called, true);
assertEquals(spy.getCalls().length, 1);
});
如果您不想仅出于测试目的添加额外的参数,您还可以使用 sinon
来包装对象上的方法。在其他 JavaScript 环境中,bar
可能可以通过全局变量(如 window
)访问,并可以通过 sinon.spy(window, "bar")
调用,但在 Deno 中,这将不起作用,而是可以 export
一个包含要测试的函数的对象。这意味着将 my_file.js
重写为类似于以下内容
// my_file.js
function bar() {/*...*/}
export const funcs = {
bar,
};
// 'foo' no longer takes a parameter, but calls 'bar' from an object
export function foo() {
funcs.bar();
}
然后在测试文件中 import
import sinon from "npm:sinon";
import { assertEquals } from "https://deno.land/[email protected]/assert/mod.ts";
import { foo, funcs } from "./my_file.js";
Deno.test("calls bar during execution of foo", () => {
// create a test spy that wraps 'bar' on the 'funcs' object
const spy = sinon.spy(funcs, "bar");
// call function 'foo' without an argument
foo();
assertEquals(spy.called, true);
assertEquals(spy.getCalls().length, 1);
});