> 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/ru/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('Ожидание', { waitLabels: 'Ожидание', errorLabels: 'Ошибка' }), '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`.
* **Middleware** — `tests/guards.test.js` покрывает каждый guard в `common/guards.js` с `(req, res, next)` заглушках.
* **Файлы статических данных** — форма и целостность. `tests/changelog.test.js`, `tests/achievements.test.js`, `tests/sections.test.js`, `tests/ip-databases.test.js`.
* **Обработчики API** — только smoke-покрытие, см. ниже.

Когда поведение меняется, обновляйте затронутые спецификации **в том же изменении**. Не откладывайте.

### Спецификации мостов

Когда вспомогательная функция находится в `common/` и переэкспортируется через `frontend/utils/`, спецификация импортирует **оба пути** и проверяет, что они совпадают. `tests/valid-ip.test.js` делает именно это, и это не даёт мосту тихо снова вырастить дублирующую реализацию.

## Что не получает спецификацию

| Вне области              | Почему                                                      |
| ------------------------ | ----------------------------------------------------------- |
| Рендеринг Vue            | Раннер Node ничего не монтирует — ни DOM, ни компоненты.    |
| Реальные сетевые запросы | Ни одна спецификация не может обращаться к живому upstream. |
| API браузера             | WebRTC, `navigator`, canvas-фингерпринтинг и прочее.        |

{% hint style="warning" %}
**Визуальные изменения нельзя протестировать самостоятельно.** Если ваше изменение визуальное, прямо скажите об этом при передаче и дайте человеку посмотреть на него в `pnpm dev`. Зелёный `pnpm check` не доказывает ничего о том, как выглядит интерфейс.
{% endhint %}

## Smoke-тесты для обработчиков API

Каждый обработчик в `api/` имеет smoke-покрытие в `tests/api-handlers.test.js`. Подход узкий и строгий:

{% hint style="danger" %}
**Никогда не обращайтесь к реальному upstream.** Проверяйте только ветки, которые возвращают **до** первого `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('отклоняет отсутствие ?mac', async () => {
        const res = createResponse();
        await macCheckerHandler(createRequest(), res);
        assert.equal(res.statusCode, 400);
        assert.deepEqual(res.body, { error: 'MAC-адрес не указан' });
    });

    it('отклоняет неверный формат MAC', async () => {
        const res = createResponse();
        await macCheckerHandler(createRequest({ query: { mac: 'not-a-mac' } }), res);
        assert.equal(res.statusCode, 400);
        assert.deepEqual(res.body, { error: 'Неверный MAC-адрес' });
    });
});
```

{% endcode %}

### Переменные окружения

Тесты, которые меняют переменную окружения, регистрируют её имя в `ENV_KEYS` массиве в верхней части файла. `beforeEach` делает резервную копию этих ключей и `afterEach` восстанавливает их в afterEach, чтобы ни одна спецификация не протекала состоянием в следующую.

### Не дублируйте middleware

Проверки Referer и валидация параметров обеспечиваются middleware, а не обработчиками. Они покрываются один раз, в `tests/guards.test.js`Спецификация обработчика не должна заново утверждать «отклоняет плохой домен» — обработчик такого никогда не видит.

По соглашению это отмечают в комментарии над `describe` блоком, как это делает блок обработчика OONI:

```js
// Наличие и форма домена обеспечиваются middleware requireValidDomain
// (tests/guards.test.js); единственная ветка обработчика до fetch — это
// защитная проверка метода.
```

### Защитные проверки метода остаются

Некоторые обработчики сохраняют `req.method !== 'GET'` проверку, хотя маршрут уже ограничивает метод. Эти проверки существуют, потому что smoke-тесты проверяют их напрямую. Оставляйте их на месте.

## Связанные спецификации, которые полезно знать

| Спецификация                                                    | Покрывает                                                                                  |
| --------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `tests/guards.test.js`                                          | Каждый middleware в `common/guards.js`                                                     |
| `tests/fetch-with-timeout.test.js`                              | Поведение таймаута upstream и прерывания                                                   |
| `tests/changelog.test.js`                                       | Форма changelog и покрытие четырёх локалей — см. [i18n](/developer/ru/development/i18n.md) |
| `tests/report-schema.test.js` / `tests/report-builders.test.js` | Конвейер диагностического отчёта, которым можно поделиться                                 |

## `pnpm check` должен быть зелёным

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

Тесты плюс реальная production-сборка. Запускайте это перед каждой передачей — PR, коммитом, запросом на ревью.

CI выполняет те же два шага (`pnpm test`, затем `pnpm run build`) при каждом push и pull request в `main` и `dev`, на Node 24, с установкой через `pnpm install --frozen-lockfile`. Зелёный локальный `check` обычно означает зелёный прогон CI.

## Далее

* [Соглашения по написанию кода](/developer/ru/development/coding-conventions.md) — правила, по которым проверяются спецификации.
* [Добавление нового инструмента](/developer/ru/development/adding-a-new-tool.md) — где тесты находятся в рамках полной функциональности.
* [Backend](/developer/ru/architecture/backend.md) — проектирование обработчиков и middleware.


---

# 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/ru/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.
