> For the complete documentation index, see [llms.txt](https://docs.ipcheck.ing/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.ipcheck.ing/developer/zh/development/testing.md).

# 测试

MyIP 使用 **Node.js 内置测试运行器**。完全没有 Jest、Vitest，也没有任何测试框架依赖。

```bash
pnpm test     # node --test tests/*.test.js
```

每个规格都位于 `tests/`，平铺式命名为 `<subject>.test.js`。可组合规格以以下前缀命名： `tests/composable-status-tone.test.js`, `tests/composable-refresh-orchestrator.test.js`.

在迭代时运行单个文件：

```bash
node --test tests/guards.test.js
```

## 规格形态

`node:test` 用于结构， `node:assert/strict` 用于断言。除了文件所需之外，不要自定义额外帮助函数：

{% code title="tests/composable-status-tone.test.js" %}

```js
// 用于 ipFieldTone 的测试 — 统一的 "状态字符串 → 语气" 映射，
// WebRtcTest / DnsLeaksTest / RuleTest / ConnectivityTest 都使用它。

import assert from 'node:assert/strict';
import { describe, it } from 'node:test';

import { ipFieldTone } from '../frontend/composables/use-status-tone.js';

describe('ipFieldTone()', () => {
  it('当值等于等待标签时返回 "wait"', () => {
    assert.equal(ipFieldTone('Waiting', { waitLabels: 'Waiting', errorLabels: 'Error' }), 'wait');
  });
});
```

{% endcode %}

和项目中的其他文件一样，规格文件以说明其覆盖内容的标题注释开头。

## 哪些内容需要编写规格

{% hint style="success" %}
**任何无需网络调用即可执行的非可视逻辑，都要配套规格——并且在同一次变更中完成。**
{% endhint %}

实际上：

* **纯函数** ——校验器、格式化器、解析器。 `tests/valid-ip.test.js`, `tests/bgp-prefix.test.js`, `tests/mtr-parse.test.js`.
* **转换器** ——任何重塑上游数据的东西。 `tests/transform-ip-data.test.js`, `tests/service-status-transform.test.js`.
* **带有可模拟输入的组合式函数** ——可通过传入值来驱动的逻辑。 `tests/composable-achievement-engine.test.js`, `tests/composable-info-mask.test.js`.
* **中间件** — `tests/guards.test.js` 涵盖其中的每一个守卫： `common/guards.js` 并使用 `(req, res, next)` 桩。
* **静态数据文件** ——结构和完整性。 `tests/changelog.test.js`, `tests/achievements.test.js`, `tests/sections.test.js`, `tests/ip-databases.test.js`.
* **API 处理器** ——仅做冒烟覆盖，见下文。

当行为变化时，更新受影响的规格 **并在同一次变更中完成**。不要拖延。

### 桥接规格

当某个帮助函数位于 `common/` 并通过 `frontend/utils/`重新导出时，规格会导入 **两个路径** 并断言它们一致。 `tests/valid-ip.test.js` 就是这样做的，这能阻止桥接层悄悄重新长出一份重复实现。

## 哪些内容不需要规格

| 范围之外    | 原因                                     |
| ------- | -------------------------------------- |
| Vue 渲染  | Node 运行器不会挂载任何东西——没有 DOM，没有组件。         |
| 真实网络调用  | 任何规格都不得访问真实上游。                         |
| 浏览器 API | WebRTC、 `navigator`、canvas 指纹识别以及类似内容。 |

{% hint style="warning" %}
**视觉改动无法自我测试。** 如果你的改动是视觉性的，在交付时请明确说明，并让人类在其中查看 `pnpm dev`。一个通过的 `pnpm check` 并不能证明 UI 的外观。
{% endhint %}

## API 处理器的冒烟测试

位于 `api/` 下的每个处理器都在 `tests/api-handlers.test.js`中有冒烟覆盖。原则很窄且严格：

{% hint style="danger" %}
**绝不访问真实上游。** 只对返回 **之前** 第一次 `fetchUpstream` 调用。
{% endhint %}

这就只剩下三类断言：

* **方法门控** — `POST` 对仅 GET 的处理器进行调用会返回 `405`.
* **参数分支** ——缺失或格式错误的输入会返回 `400`.
* **“API key missing”的提前返回** ——处理器会在调用外部前直接退出。

该文件提供两个所有处理器测试都会复用的桩：

{% code title="tests/api-handlers.test.js" %}

```js
function createRequest(options = {}) {
    const method = options.method || 'GET';
    const query = options.query || {};
    const referer = Object.hasOwn(options, 'referer') ? options.referer : 'http://localhost/';
    const headers = {};
    if (referer !== undefined) headers.referer = referer;
    return { method, headers, query, body: options.body };
}

function createResponse() {
    return {
        statusCode: 200,
        body: undefined,
        status(code) { this.statusCode = code; return this; },
        json(payload) { this.body = payload; return this; },
        send(payload) { this.body = payload; return this; },
    };
}
```

{% endcode %}

一个完整的处理器测试块如下：

{% code title="tests/api-handlers.test.js" %}

```js
describe('mac-checker handler', () => {
    it('rejects missing ?mac', async () => {
        const res = createResponse();
        await macCheckerHandler(createRequest(), res);
        assert.equal(res.statusCode, 400);
        assert.deepEqual(res.body, { error: 'No MAC address provided' });
    });

    it('rejects invalid MAC format', async () => {
        const res = createResponse();
        await macCheckerHandler(createRequest({ query: { mac: 'not-a-mac' } }), res);
        assert.equal(res.statusCode, 400);
        assert.deepEqual(res.body, { error: 'Invalid MAC address' });
    });
});
```

{% endcode %}

### 环境变量

会切换环境变量的测试，会把该变量名登记到文件顶部的 `ENV_KEYS` 数组中。 `beforeEach` 会备份这些键，并且 `afterEach` 恢复它们，因此不会有规格把状态泄漏到下一个。

### 不要重复中间件

Referer 检查和参数校验由中间件而非处理器强制执行。它们只覆盖一次，位于 `tests/guards.test.js`。处理器规格不应再次断言“拒绝坏域名”——因为处理器根本不会见到它。

惯例是在上方注释中注明这一点，位于 `describe` 块之上，就像 OONI 处理器的块那样：

```js
// Domain presence/shape is enforced by requireValidDomain middleware
// （tests/guards.test.js）；处理器唯一的预取前分支是
// 防御性方法门控。
```

### 防御性方法门控保持

某些处理器会保留一个 `req.method !== 'GET'` 检查，即使路由已经限制了方法。之所以保留这些门控，是因为冒烟测试会直接对它们做断言。保留它们。

## 值得了解的相关规格

| 规格                                                              | 覆盖内容                                                      |
| --------------------------------------------------------------- | --------------------------------------------------------- |
| `tests/guards.test.js`                                          | 位于其中的每个中间件 `common/guards.js`                             |
| `tests/fetch-with-timeout.test.js`                              | 上游超时和中止行为                                                 |
| `tests/changelog.test.js`                                       | 变更日志结构和四语言覆盖——见 [i18n](/developer/zh/development/i18n.md) |
| `tests/report-schema.test.js` / `tests/report-builders.test.js` | 可共享的诊断报告流水线                                               |

## `pnpm check` 必须通过

```bash
pnpm check     # pnpm test && pnpm build
```

测试加上一次真实的生产构建。每次交付前都要运行它——无论是 PR、提交还是审查请求。

CI 会运行相同的两个步骤（`pnpm test`，然后 `pnpm run build`）会在每次 push 和 pull request 时针对 `main` 和 `dev`进行，在 Node 24 上，使用 `pnpm install --frozen-lockfile`。本地通过的 `check` 通常意味着 CI 也会通过。

## 下一步

* [编码规范](/developer/zh/development/coding-conventions.md) ——规格所依据的规则。
* [添加新工具](/developer/zh/development/adding-a-new-tool.md) ——测试在完整功能中的位置。
* [后端](/developer/zh/architecture/backend.md) ——处理器和中间件设计。


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.ipcheck.ing/developer/zh/development/testing.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
