跳转到主要内容
本页内容

测试

Deno 提供了一个内置的测试运行器,用于编写和运行 JavaScript 和 TypeScript 测试。 这使得确保代码可靠并按预期运行变得容易,而无需安装任何额外的依赖项或工具。deno test 运行器允许您对每个测试的权限进行细粒度控制,确保代码不会执行任何意外操作。

除了内置的测试运行器,您还可以将 JS 生态系统中的其他测试运行器(例如 Jest、Mocha 或 AVA)与 Deno 一起使用。但是,本文档不会介绍这些内容。

编写测试 跳转到标题

要在 Deno 中定义测试,您可以使用 Deno.test() 函数。以下是一些示例

my_test.ts
import { assertEquals } from "jsr:@std/assert";

Deno.test("simple test", () => {
  const x = 1 + 2;
  assertEquals(x, 3);
});

import { delay } from "jsr:@std/async";

Deno.test("async test", async () => {
  const x = 1 + 2;
  await delay(100);
  assertEquals(x, 3);
});

Deno.test({
  name: "read file test",
  permissions: { read: true },
  fn: () => {
    const data = Deno.readTextFileSync("./somefile.txt");
    assertEquals(data, "expected content");
  },
});

如果您更喜欢类似 Jest 的 expect 断言风格,Deno 标准库提供了一个 expect 函数,可以用来代替 assertEquals

my_test.ts
import { expect } from "jsr:@std/expect";
import { add } from "./add.js";

Deno.test("add function adds two numbers correctly", () => {
  const result = add(2, 3);
  expect(result).toBe(5);
});

运行测试 跳转到标题

要运行测试,请使用 deno test 子命令。

如果不带文件名或目录名运行,此子命令将自动查找并执行当前目录(递归)中所有匹配 glob 模式 {*_,*.,}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

# Pass additional arguments to the test file that are visible in `Deno.args`
deno test my_test.ts -- -e --foo --bar

# Provide permission for deno to read from the filesystem, which is necessary
# for the final test above to pass
deno test --allow-read my_test.ts

测试步骤 跳转到标题

Deno 还支持测试步骤,它允许您将测试分解成更小、更易于管理的部分。这对于测试中的设置和拆卸操作非常有用。

Deno.test("database operations", async (t) => {
  using db = await openDatabase();
  await t.step("insert user", async () => {
    // Insert user logic
  });
  await t.step("insert book", async () => {
    // Insert book logic
  });
});

命令行过滤 跳转到标题

Deno 允许您使用命令行上的 --filter 选项运行特定测试或测试组。此选项接受字符串或模式来匹配测试名称。过滤不会影响步骤;如果测试名称与过滤器匹配,则其所有步骤都将执行。

考虑以下测试

Deno.test("my-test", () => {});
Deno.test("test-1", () => {});
Deno.test("test-2", () => {});

按字符串过滤 跳转到标题

要运行名称中包含单词“my”的所有测试,请使用

deno test --filter "my" tests/

此命令将执行 my-test,因为其名称中包含单词“my”。

按模式过滤 跳转到标题

要运行与特定模式匹配的测试,请使用

deno test --filter "/test-*\d/" tests/

此命令将运行 test-1test-2,因为它们与后跟数字的模式 test-* 匹配。

要指示您正在使用模式(正则表达式),请用正斜杠 / 将过滤器值括起来,就像 JavaScript 的正则表达式语法一样。

在配置文件中包含和排除测试文件 跳转到标题

您还可以通过在 Deno 配置文件 中指定要包含或排除的路径来过滤测试。

例如,如果您只想测试 src/fetch_test.tssrc/signal_test.ts 并排除 out/ 中的所有内容

{
  "test": {
    "include": [
      "src/fetch_test.ts",
      "src/signal_test.ts"
    ]
  }
}

或者更可能的情况是

{
  "test": {
    "exclude": ["out/"]
  }
}

测试定义选择 跳转到标题

Deno 提供了两个选项用于在测试定义本身中选择测试:忽略测试和专注于特定测试。

忽略/跳过测试 跳转到标题

您可以使用测试定义中的 ignore 布尔值根据特定条件忽略某些测试。如果将 ignore 设置为 true,则将跳过该测试。例如,如果您只想在特定操作系统上运行测试,这将非常有用。

Deno.test({
  name: "do macOS feature",
  ignore: Deno.build.os !== "darwin", // This test will be ignored if not running on macOS
  fn() {
    // do MacOS feature here
  },
});

如果您想要忽略一个测试而不传递任何条件,您可以使用来自 Deno.test 对象的 ignore() 函数。

Deno.test.ignore("my test", () => {
  // your test code
});

仅运行特定测试 跳转到标题

如果您想要专注于一个特定的测试并忽略其余测试,您可以使用 only 选项。这会告诉测试运行器只运行 only 设置为 true 的测试。多个测试可以设置此选项。然而,如果有任何测试被标记为 only,则整个测试运行将始终失败,因为这旨在作为调试的临时措施。

Deno.test.only("my test", () => {
  // some test code
});

或者

Deno.test({
  name: "Focus on this test only",
  only: true, // Only this test will run
  fn() {
    // test complicated stuff here
  },
});

快速失败 跳转到标题

如果您有一个长时间运行的测试套件,并且希望它在第一次失败时停止,您可以在运行套件时指定 --fail-fast 标志。

deno test --fail-fast

这将导致测试运行器在第一次测试失败后停止执行。

报告器 跳转到标题

Deno 包含三个内置报告器来格式化测试输出

  • pretty(默认):提供详细且可读的输出。
  • dot:提供简洁的输出,有助于快速查看测试结果。
  • junit:以 JUnit XML 格式生成输出,这对于与 CI/CD 工具集成非常有用。

您可以使用 --reporter 标志指定要使用的报告器

# Use the default pretty reporter
deno test

# Use the dot reporter for concise output
deno test --reporter=dot

# Use the JUnit reporter
deno test --reporter=junit

此外,您可以使用 --junit-path 标志将 JUnit 报告写入文件,同时仍在终端中获得人类可读的输出。

deno test --junit-path=./report.xml

Spy、Mock(测试替身)、Stub 和伪造时间 跳转到标题

Deno 标准库 提供了一组函数来帮助您编写涉及 Spy、Mock 和 Stub 的测试。请查看 JSR 上的 @std/testing 文档,以获取有关每个实用程序的更多信息。

覆盖率 跳转到标题

如果您在启动 deno test 时指定 --coverage 标志,Deno 会将测试覆盖率收集到代码的目录中。此覆盖率信息直接从 V8 JavaScript 引擎获取,确保了高精度。

然后可以使用 deno coverage 工具将内部格式进一步处理为众所周知的格式,例如 lcov

行为驱动开发 跳转到标题

使用 @std/testing/bdd 模块,您可以使用熟悉的格式编写测试,以便对测试进行分组并添加其他 JavaScript 测试框架(如 Jasmine、Jest 和 Mocha)使用的 setup/teardown 钩子。

describe 函数创建一个块,将几个相关的测试组合在一起。it 函数注册一个单独的测试用例。例如

import { describe, it } from "jsr:@std/testing/bdd";
import { expect } from "jsr:@std/expect";
import { add } from "./add.js";

describe("add function", () => {
  it("adds two numbers correctly", () => {
    const result = add(2, 3);
    expect(result).toBe(5);
  });

  it("handles negative numbers", () => {
    const result = add(-2, -3);
    expect(result).toBe(-5);
  });
});

请查看 JSR 上的 文档,以获取有关这些函数和钩子的更多信息。

文档测试 跳转到标题

Deno 允许您评估用 JSDoc 或 Markdown 文件编写的代码片段。这确保了文档中的示例是最新的且可运行的。

示例代码块 跳转到标题

example.ts
/**
 * # Examples
 *
 * ```ts
 * import { assertEquals } from "jsr:@std/assert/equals";
 *
 * const sum = add(1, 2);
 * assertEquals(sum, 3);
 * ```
 */
export function add(a: number, b: number): number {
  return a + b;
}

三个反引号标记代码块的开始和结束,语言由语言标识符属性确定,该属性可以是以下之一

  • js
  • javascript
  • mjs
  • cjs
  • jsx
  • ts
  • typescript
  • mts
  • cts
  • tsx

如果没有指定语言标识符,则会从提取代码块的源文档的媒体类型推断语言。

deno test --doc example.ts

上述命令将提取此示例,并将其转换为如下所示的伪测试用例

example.ts$4-10.ts
import { assertEquals } from "jsr:@std/assert/equals";
import { add } from "file:///path/to/example.ts";

Deno.test("example.ts$4-10.ts", async () => {
  const sum = add(1, 2);
  assertEquals(sum, 3);
});

然后将其作为独立模块运行,该模块与被记录的模块位于同一目录中。

只想进行类型检查?

如果您只想对 JSDoc 和 Markdown 文件中的代码片段进行类型检查而不实际运行它们,您可以使用带有 --doc 选项(用于 JSDoc)或 --doc-only 选项(用于 Markdown)的 deno check 命令。

自动导入已导出的项目 跳转到标题

查看上面生成的测试代码,您会注意到它包含用于导入 add 函数的 import 语句,即使原始代码块中没有该语句。在记录模块时,使用相同的名称自动将从模块导出的任何项目包含在生成的测试代码中。

假设我们有以下模块

example.ts
/**
 * # Examples
 *
 * ```ts
 * import { assertEquals } from "jsr:@std/assert/equals";
 *
 * const sum = add(ONE, getTwo());
 * assertEquals(sum, 3);
 * ```
 */
export function add(a: number, b: number): number {
  return a + b;
}

export const ONE = 1;
export default function getTwo() {
  return 2;
}

这将转换为以下测试用例

example.ts$4-10.ts
import { assertEquals } from "jsr:@std/assert/equals";
import { add, ONE }, getTwo from "file:///path/to/example.ts";

Deno.test("example.ts$4-10.ts", async () => {
  const sum = add(ONE, getTwo());
  assertEquals(sum, 3);
});

跳过代码块 跳转到标题

您可以通过添加 ignore 属性来跳过代码块的评估。

/**
 * This code block will not be run.
 *
 * ```ts ignore
 * await sendEmail("[email protected]");
 * ```
 */
export async function sendEmail(to: string) {
  // send an email to the given address...
}

清理器 跳转到标题

测试运行器提供了几种清理器,以确保测试以合理且预期的方式运行。

资源清理器 跳转到标题

资源清理器确保在测试期间创建的所有 I/O 资源都被关闭,以防止泄漏。

I/O 资源是指 Deno.FsFile 句柄、网络连接、fetch 主体、计时器以及其他不会自动进行垃圾回收的资源。

使用完资源后,应始终关闭它们。例如,要关闭文件

const file = await Deno.open("hello.txt");
// Do something with the file
file.close(); // <- Always close the file when you are done with it

要关闭网络连接

const conn = await Deno.connect({ hostname: "example.com", port: 80 });
// Do something with the connection
conn.close(); // <- Always close the connection when you are done with it

要关闭 fetch 主体

const response = await fetch("https://example.com");
// Do something with the response
await response.body?.cancel(); // <- Always cancel the body when you are done with it, if you didn't consume it otherwise

默认情况下启用此清理器,但可以在此测试中使用 sanitizeResources: false 禁用它。

Deno.test({
  name: "leaky resource test",
  async fn() {
    await Deno.open("hello.txt");
  },
  sanitizeResources: false,
});

异步操作清理器 跳转到标题

异步操作清理器确保在测试结束之前完成测试中启动的所有异步操作。这一点很重要,因为如果未等待异步操作,则测试将在操作完成之前结束,即使操作实际上可能已失败,测试也会被标记为成功。

您应该始终在测试中等待所有异步操作。例如

Deno.test({
  name: "async operation test",
  async fn() {
    await new Promise((resolve) => setTimeout(resolve, 1000));
  },
});

默认情况下启用此清理器,但可以使用 sanitizeOps: false 禁用它。

Deno.test({
  name: "leaky operation test",
  fn() {
    crypto.subtle.digest(
      "SHA-256",
      new TextEncoder().encode("a".repeat(100000000)),
    );
  },
  sanitizeOps: false,
});

退出清理器 跳转到标题

退出清理器可确保测试代码不会调用 Deno.exit(),这可能会导致测试被错误地标记为成功。

此清理器默认启用,但可以使用 sanitizeExit: false 禁用。

Deno.test({
  name: "false success",
  fn() {
    Deno.exit(0);
  },
  sanitizeExit: false,
});

// This test never runs, because the process exits during "false success" test
Deno.test({
  name: "failing test",
  fn() {
    throw new Error("this test fails");
  },
});

快照测试 跳转到标题

Deno 标准库 包含一个 快照模块,允许开发者通过将值与参考快照进行比较来编写测试。这些快照是原始值的序列化表示形式,并与测试文件一起存储。

快照测试可以用很少的代码捕获各种错误。它在难以精确表达断言内容、需要大量代码或测试断言预期经常更改的情况下特别有用。