在本页

在 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)

注意

  1. 测试步骤 **必须在父测试/步骤函数解析之前等待**,否则你会得到运行时错误。
  2. 测试步骤不能并发运行,除非兄弟步骤或父测试上的消毒器被禁用。
  3. 如果嵌套步骤,请确保为父步骤指定一个参数。
    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.tssrc/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";

假设我们有两个函数 foobar,并且想要断言 barfoo 执行期间被调用。使用 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);
});