deno.com
本页内容

编写测试

在软件开发中,测试至关重要,它能确保你的代码按预期工作,并在你做出更改后继续正常运行。测试验证你的函数、模块和应用程序行为正确,能够妥善处理边缘情况,并保持预期的性能特征。

为什么测试很重要 跳转到标题

测试你的代码可以帮助你在代码发布到生产环境之前捕获错误、问题或回归,从而节省时间和资源。测试还有助于规划应用程序的逻辑,它们可以作为代码预期用途的人类可读描述。

Deno 提供了内置的测试能力,使得在你的项目中实现健壮的测试实践变得简单直接。

使用 Deno.test 编写测试 跳转到标题

在 Deno 中定义测试非常简单——使用 Deno.test() 函数将你的测试注册到测试运行器。此函数接受测试名称和函数,或包含更详细选项的配置对象。所有符合 *_test.{ts,js,mjs,jsx,tsx}*.test.{ts,js,mjs,jsx,tsx} 等模式的文件中的测试函数,在你运行 deno test 命令时都会被自动发现和执行。

以下是定义测试的基本方式

// Basic test with a name and function
Deno.test("my first test", () => {
  // Your test code here
});

// Test with configuration options
Deno.test({
  name: "my configured test",
  fn: () => {
    // Your test code here
  },
  ignore: false, // Optional: set to true to skip this test
  only: false, // Optional: set to true to only run this test
  permissions: { // Optional: specify required permissions
    read: true,
    write: false,
  },
});

一个简单的测试示例 跳转到标题

让我们从一个简单的测试开始。创建一个名为 main_test.ts 的文件,我们将在其中使用 Deno 的测试 API 和来自 Deno 标准库assertEquals 函数来测试一个基本的加法操作。

我们使用 Deno.test 并提供一个描述测试功能的名称

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

// Function we want to test
function add(a: number, b: number): number {
  return a + b;
}

Deno.test("basic addition test", () => {
  // Arrange - set up the test data
  const a = 1;
  const b = 2;

  // Act - call the function being tested
  const result = add(a, b);

  // Assert - verify the result is what we expect
  assertEquals(result, 3);
});

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

deno test hello_test.ts

你应该会看到表明测试已通过的输出

running 1 test from ./hello_test.ts
basic addition test ... ok (2ms)

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out (2ms)

尝试更改函数实现以使测试失败

function add(a: number, b: number): number {
  return a - b; // Changed from addition to subtraction
}

你会看到一个清晰显示问题所在的错误消息

running 1 test from ./hello_test.ts
basic addition test ... FAILED (3ms)

failures:

basic addition test => ./hello_test.ts:12:3
error: AssertionError: Values are not equal:
    
    [Diff] Actual / Expected
    
    -   -1
    +   3

  at assertEquals (https://jsr.deno.org.cn/@std/assert@0.218.2/assert_equals.ts:31:9)
  at Object.fn (file:///path/to/hello_test.ts:12:3)
  at asyncOpSanitizer (ext:core/01_core.js:199:13)
  at Object.sanitizeOps (ext:core/01_core.js:219:15)
  at runTest (ext:test/06_test_runner.js:319:29)
  at test (ext:test/06_test_runner.js:593:7)

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out (3ms)

这种清晰的反馈有助于你快速识别并修复代码中的问题。

测试结构和组织 跳转到标题

Deno 会自动查找并运行符合 *_test.{ts,js,mjs,jsx,tsx}*.test.{ts,js,mjs,jsx,tsx} 等命名模式的测试。组织测试文件的方法有很多,我们建议将单元测试与它们所测试的代码放在一起,并将集成测试和配置保存在一个 tests 目录中。这使得单元测试能够立即被发现并简化了导入,同时保持了不同类型测试之间的分离。

以下是项目与测试结合的结构示例

my-deno-project/
├── src/
│   ├── models/
│   │   ├── user.ts
│   │   ├── user_test.ts          // Unit tests for user model
│   │   ├── product.ts
│   │   └── product_test.ts       // Unit tests for product model
│   ├── services/
│   │   ├── auth-service.ts
│   │   ├── auth-service_test.ts  // Unit tests for auth service
│   │   ├── data-service.ts
│   │   └── data-service_test.ts  // Unit tests for data service
│   └── utils/
│       ├── helpers.ts
│       └── helpers_test.ts       // Unit tests for helpers
├── tests/
│   ├── integration/              // Integration tests directory
│   │   ├── api_test.ts           // Tests API endpoints
│   │   └── db_test.ts            // Tests database interactions
│   ├── e2e/                      // End-to-end tests
│   │   └── user_flow_test.ts     // Tests complete user workflows
│   └── fixtures/                 // Shared test data and utilities
│       ├── test_data.ts          // Test data used across tests
│       └── setup.ts              // Common setup functions
├── main.ts
└── deno.json                     // Project configuration

这种结构为测试配置提供了一个集中位置,同时保持了单元测试与其相关文件共存的优势。采用这种结构,你可以

# Run all tests
deno test

# Run only unit tests
deno test src/

# Run only integration tests
deno test tests/integration/

# Run specific module tests
deno test src/models/

# Run a specific test file
deno test src/models/user_test.ts

断言 跳转到标题

断言是有效测试的基石,它允许你验证代码是否按预期运行。它们检查特定条件是否为真,如果不是,则抛出错误,导致测试失败。良好的断言清晰、具体,并有助于在测试失败时准确识别问题所在。

Deno 的核心库中不包含断言,但你可以从 Deno 标准库中导入它们。

import {
  assertArrayIncludes, // Check that array contains value
  assertEquals, // Check that values are equal
  assertExists, // Check that value is not null or undefined
  assertMatch, // Check that string matches regex pattern
  assertNotEquals, // Check that values are not equal
  assertObjectMatch, // Check that object has expected properties
  assertRejects, // Check that Promise rejects
  assertStrictEquals, // Check that values are strictly equal (===)
  assertStringIncludes, // Check that string contains substring
  assertThrows, // Check that function throws an error
} from "jsr:@std/assert";

Deno.test("assertion examples", () => {
  // Basic assertions
  assertEquals(1 + 1, 2);
  assertNotEquals("hello", "world");
  assertExists("Hello");

  // String assertions
  assertStringIncludes("Hello, world!", "world");
  assertMatch("deno@1.0.0", /^deno@\d+\.\d+\.\d+$/);

  // Object assertions
  assertObjectMatch(
    { name: "Jane", age: 25, city: "Tokyo" },
    { name: "Jane" }, // Only checks specified properties
  );

  // Strict equality (type + value)
  assertStrictEquals("deno", "deno");

  // Error assertions
  assertThrows(
    () => {
      throw new Error("Something went wrong");
    },
    Error,
    "Something went wrong",
  );
});

对于喜欢流畅断言(Jest 用户所熟悉)的用户,可以使用 expect 模块

import { expect } from "jsr:@std/expect";

Deno.test("expect style assertions", () => {
  // Basic matchers
  expect(5).toBe(5);
  expect({ name: "deno" }).toEqual({ name: "deno" });

  // Collection matchers
  expect([1, 2, 3]).toContain(2);

  // Truthiness matchers
  expect(true).toBeTruthy();
  expect(0).toBeFalsy();
  expect(null).toBeNull();
  expect(undefined).toBeUndefined();

  // Number matchers
  expect(100).toBeGreaterThan(99);
  expect(1).toBeLessThan(2);

  // String matchers
  expect("Hello world").toMatch(/world/);

  // Function/error matchers
  expect(() => {
    throw new Error("fail");
  }).toThrow();
});

真实世界的示例 跳转到标题

这是一个测试处理用户数据函数的更真实的例子

// user_processor.ts
export function validateUser(user: any): { valid: boolean; errors: string[] } {
  const errors: string[] = [];

  if (!user.name || typeof user.name !== "string") {
    errors.push("Name is required and must be a string");
  }

  if (!user.email || !user.email.includes("@")) {
    errors.push("Valid email is required");
  }

  if (
    user.age !== undefined && (typeof user.age !== "number" || user.age < 18)
  ) {
    errors.push("Age must be a number and at least 18");
  }

  return {
    valid: errors.length === 0,
    errors,
  };
}

// user_processor_test.ts
import { assertEquals } from "jsr:@std/assert";
import { validateUser } from "./user_processor.ts";

Deno.test("validateUser", async (t) => {
  await t.step("should validate a correct user object", () => {
    const user = {
      name: "John Doe",
      email: "john@example.com",
      age: 30,
    };

    const result = validateUser(user);
    assertEquals(result.valid, true);
    assertEquals(result.errors.length, 0);
  });

  await t.step("should return errors for invalid user", () => {
    const user = {
      name: "",
      email: "invalid-email",
      age: 16,
    };

    const result = validateUser(user);
    assertEquals(result.valid, false);
    assertEquals(result.errors.length, 3);
    assertEquals(result.errors[0], "Name is required and must be a string");
    assertEquals(result.errors[1], "Valid email is required");
    assertEquals(result.errors[2], "Age must be a number and at least 18");
  });

  await t.step("should handle missing properties", () => {
    const user = {
      name: "Jane Doe",
      // email and age missing
    };

    const result = validateUser(user);
    assertEquals(result.valid, false);
    assertEquals(result.errors.length, 1);
    assertEquals(result.errors[0], "Valid email is required");
  });
});

异步测试 跳转到标题

Deno 自然地处理异步测试。只需将你的测试函数设为 async 并使用 await

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

Deno.test("async test example", async () => {
  const response = await fetch("https://deno.land");
  const status = response.status;
  assertEquals(status, 200);
});

测试异步函数 跳转到标题

测试返回 Promise 的函数时,你应始终等待结果。

// async-function.ts
export async function fetchUserData(userId: string) {
  const response = await fetch(`https://api.example.com/users/${userId}`);
  if (!response.ok) {
    throw new Error(`Failed to fetch user: ${response.status}`);
  }
  return await response.json();
}

// async-function_test.ts
import { assertEquals, assertRejects } from "jsr:@std/assert";
import { fetchUserData } from "./async-function.ts";

Deno.test("fetchUserData success", async () => {
  // Mock the fetch function for testing
  globalThis.fetch = async (url: string) => {
    const data = JSON.stringify({ id: "123", name: "Test User" });
    return new Response(data, { status: 200 });
  };

  const userData = await fetchUserData("123");
  assertEquals(userData.id, "123");
  assertEquals(userData.name, "Test User");
});

Deno.test("fetchUserData failure", async () => {
  // Mock the fetch function to simulate an error
  globalThis.fetch = async (url: string) => {
    return new Response("Not Found", { status: 404 });
  };

  await assertRejects(
    async () => await fetchUserData("nonexistent"),
    Error,
    "Failed to fetch user: 404",
  );
});

测试中的模拟 跳转到标题

模拟是将待测代码与其依赖项隔离的重要技术。Deno 提供了内置工具和第三方库来创建模拟。

基本模拟 跳转到标题

你可以通过用自己的实现替换函数或对象来创建简单的模拟。这使你能够控制依赖项的行为,并测试你的代码如何与它们交互。

// Example of a module with a function we want to mock
const api = {
  fetchData: async () => {
    const response = await fetch("https://api.example.com/data");
    return response.json();
  },
};

// In your test file
Deno.test("basic mocking example", async () => {
  // Store the original function
  const originalFetchData = api.fetchData;

  // Replace with mock implementation
  api.fetchData = async () => {
    return { id: 1, name: "Test Data" };
  };

  try {
    // Test using the mock
    const result = await api.fetchData();
    assertEquals(result, { id: 1, name: "Test Data" });
  } finally {
    // Restore the original function
    api.fetchData = originalFetchData;
  }
});

使用 Spy 函数 跳转到标题

Spies 允许你跟踪函数调用而不改变它们的行为

import { spy } from "jsr:@std/testing/mock";

Deno.test("spy example", () => {
  // Create a spy on console.log
  const consoleSpy = spy(console, "log");

  // Call the function we're spying on
  console.log("Hello");
  console.log("World");

  // Verify the function was called correctly
  assertEquals(consoleSpy.calls.length, 2);
  assertEquals(consoleSpy.calls[0].args, ["Hello"]);
  assertEquals(consoleSpy.calls[1].args, ["World"]);

  // Restore the original function
  consoleSpy.restore();
});

有关更高级的模拟技术,请查阅我们的Deno 模拟专用指南

覆盖率 跳转到标题

代码覆盖率是一种指标,可帮助你了解代码有多少部分正在被测试。它衡量在测试期间代码的哪些行、函数和分支被执行,让你深入了解可能缺乏适当测试的区域。

覆盖率分析有助于你

  • 识别代码库中未经测试的部分
  • 确保关键路径有测试
  • 在进行更改时防止回归
  • 衡量测试随时间推移的进展

注意

高覆盖率并不能保证高质量的测试。它仅仅显示了哪些代码被执行了,而不是你的断言是否有意义或者边缘情况是否得到了正确处理。

Deno 提供了内置的覆盖率工具来帮助你分析测试覆盖率。要收集覆盖率信息

deno test --coverage=coverage_dir

这会在指定目录(此处为 coverage_dir)中生成覆盖率数据。要查看人类可读的报告

deno coverage coverage_dir

你会看到类似以下的输出

file:///projects/my-project/src/utils.ts 85.7% (6/7)
file:///projects/my-project/src/models/user.ts 100.0% (15/15)
file:///projects/my-project/src/services/auth.ts 78.3% (18/23)

total: 87.5% (39/45)

为了获得更详细的洞察,你还可以生成 HTML 报告

deno coverage --html coverage_dir

这会在指定目录中创建一个交互式 HTML 报告,精确显示哪些行被覆盖以及哪些未被覆盖。

默认情况下,覆盖率工具会自动排除

  • 测试文件(匹配 test.tstest.js 等模式)
  • 远程文件(那些不以 file: 开头的文件)

这确保你的覆盖率报告侧重于你的应用程序代码,而不是测试文件或外部依赖项。

覆盖率配置 跳转到标题

你可以使用 --exclude 标志从覆盖率报告中排除文件

deno coverage --exclude="test_,vendor/,_build/,node_modules/" coverage_dir

与 CI 集成 跳转到标题

对于持续集成环境,你可能希望强制执行最低覆盖率阈值

# In your GitHub Actions workflow
- name: Run tests with coverage
  run: deno test --coverage=coverage_dir

- name: Check coverage meets threshold
  run: |
    COVERAGE=$(deno coverage coverage_dir | grep "total:" | grep -o '[0-9]\+\.[0-9]\+')
    if (( $(echo "$COVERAGE < 80" | bc -l) )); then
      echo "Test coverage is below 80%: $COVERAGE%"
      exit 1
    fi

在处理测试覆盖率时,请记住设定实际目标,追求有意义的覆盖率和高质量的测试,而不是盲目追求 100% 的覆盖率。

与其他测试框架的比较 跳转到标题

如果你来自其他 JavaScript 测试框架,以下是 Deno 测试能力与它们的比较

功能 Deno Jest Mocha Jasmine
设置 内置 需要安装 需要安装 需要安装
语法 Deno.test() test()describe() it()describe() it()describe()
断言 来自标准库 内置 expect 需要断言库 内置 expect
模拟 来自标准库 内置 jest.mock() 需要 sinon 或类似库 内置 spies
异步支持 原生 需要特殊处理 支持 Promise 支持 Promise
文件监听 --watch 标志 监听模式 需要 nodemon 需要额外工具
代码覆盖率 内置 内置 需要 istanbul 需要 istanbul

测试风格比较 跳转到标题

Deno

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

Deno.test("add function", () => {
  assertEquals(1 + 2, 3);
});

Jest

test("add function", () => {
  expect(1 + 2).toBe(3);
});

Mocha

import { assert } from "chai";

describe("math", () => {
  it("should add numbers", () => {
    assert.equal(1 + 2, 3);
  });
});

Jasmine

describe("math", () => {
  it("should add numbers", () => {
    expect(1 + 2).toBe(3);
  });
});

后续步骤 跳转到标题

🦕 Deno 的内置测试能力使得编写和运行测试变得容易,而无需安装额外的测试框架或工具。通过遵循本教程中概述的模式和实践,你可以确保你的 Deno 应用程序经过充分测试且可靠。

有关 Deno 中测试的更多信息,请查阅

您找到所需内容了吗?

隐私政策