Введение
API службы поддержки принимает корректные запросы, но завершается с ошибкой при некорректном JSON. Вы превратите это сообщение об ошибке в падающий тест, исправите границу разбора данных и расширите набор тестов, чтобы сохранить проверку входных данных и обработку ошибок внешнего сервиса.
Эта независимая виртуальная машина содержит небольшой API, основанный на концепциях маршрутизации из лабораторной работы «Создание API запросов в службу поддержки», с одной намеренно оставленной регрессией. Node.js 22.22.0, Wrangler 4.131.1 и Miniflare 4.20260730.0 уже подготовлены в /home/labex/project/worker-tests. Вы самостоятельно напишете тесты с помощью встроенного средства запуска тестов Node и выполните обработчик в среде выполнения workerd через Miniflare. Для работы понадобятся базовые знания JavaScript и предыдущая лабораторная работа по API; утверждения тестов, хуки и изоляция фикстур объясняются здесь.
Это локальная исполняемая лабораторная работа. Авторизация в аккаунте Cloudflare, удалённые ресурсы и предыдущая виртуальная машина не нужны. Все исходящие запросы Worker перехватываются локальной фикстурой, а среды выполнения удаляются после каждого теста.
Напишите тесты для среды выполнения Workers
На этом этапе вы создадите небольшой набор тестов для готового API службы поддержки. Тесты запускаются в средстве запуска тестов Node, но запросы выполняются внутри среды workerd Miniflare, а не за счёт прямого импорта обработчика в Node.
Перейдите в независимый проект и изучите готовый обработчик и зафиксированные зависимости:
cd /home/labex/project/worker-tests
node --version
npx wrangler --version
npm ls miniflare --depth=0
cat src/index.js
Ожидаются Node v22.22.0, Wrangler 4.131.1 и прямая зависимость Miniflare версии 4.20260730.0. Эти инструменты уже установлены. На своём компьютере добавьте точную версию зависимости для тестов командой npm install --save-dev miniflare@4.20260730.0; если файл блокировки уже существует, используйте npm ci. В этой лабораторной работе API версии 4.x и поддерживаемая дата совместимости зафиксированы намеренно, чтобы не зависеть от изменяющегося тега latest.
Создайте файл test/support.test.mjs. Указанный heredoc записывает модуль без изменений. test объявляет тестовый случай, assert.equal проверяет скалярное значение, а assert.deepEqual сравнивает структурированный JSON. Каждый асинхронный тест дожидается ответа перед выполнением проверок.
beforeEach запускает новую среду выполнения и очищает список вызовов; afterEach удаляет среду выполнения даже при сбое теста. dispatchFetch отправляет внутрипроцессный тестовый запрос. Его имя хоста не обозначает развёрнутый Worker. outboundService перехватывает каждый вызов fetch из Worker и возвращает ответ локальной фикстуры; запросы в Интернет не отправляются. Он проверяет ожидаемые URL и метод внешнего сервиса и записывает нормализованный запрос. cf: false отключает получение демонстрационных метаданных запроса Cloudflare. Вход в аккаунт, удалённая привязка и развёртывание в облаке не используются.
cat > test/support.test.mjs <<'JS'
import {test, beforeEach, afterEach} from 'node:test';
import assert from 'node:assert/strict';
import {fileURLToPath} from 'node:url';
import {Miniflare} from 'miniflare';
let mf;
let calls;
beforeEach(() => {
calls = [];
mf = new Miniflare({
modules: true,
scriptPath: fileURLToPath(new URL('../src/index.js', import.meta.url)),
compatibilityDate: '2026-07-30',
cf: false,
bindings: {UPSTREAM_URL: 'https://tickets.test'},
outboundService: async (request) => {
assert.equal(request.url, 'https://tickets.test/tickets');
assert.equal(request.method, 'POST');
const body = await request.json();
calls.push(body);
if (body.subject === 'simulate-outage') {
return new Response('SIMULATED_INTERNAL_DETAIL', {status: 503});
}
return Response.json({ticket: `demo-${calls.length}`, subject: body.subject}, {status: 201});
}
});
});
afterEach(async () => { await mf.dispose(); });
test('health stays public', async () => {
const response = await mf.dispatchFetch('http://worker.test/health');
assert.equal(response.status, 200);
assert.deepEqual(await response.json(), {status: 'ok'});
assert.equal(calls.length, 0);
});
test('valid request reaches the local fixture', async () => {
const response = await mf.dispatchFetch('http://worker.test/requests', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({subject: ' Printer offline '})
});
assert.equal(response.status, 201);
assert.deepEqual(await response.json(), {ticket: 'demo-1', subject: 'Printer offline'});
assert.deepEqual(calls, [{subject: 'Printer offline'}]);
});
JS
Запустите стандартную команду тестирования Node; параметр --test-reporter=spec выводит понятные имена тестов и итоговые результаты:
node --test --test-reporter=spec test/support.test.mjs
Ожидаются два успешно пройденных теста и ноль ошибок. Запрос к /health не должен обращаться к внешнему сервису. Корректный запрос должен вернуть demo-1 и ровно один раз отправить тему после удаления пробелов по краям. Эти начальные тесты пока не проверяют некорректный JSON. Используйте проверку, чтобы убедиться в работоспособности набора тестов и независимом поведении среды выполнения.
Подробнее о базовых интерфейсах см. в Miniflare API и параметре outbound service. Поведение сети в рабочей среде всё равно нужно отдельно проверять после развёртывания.
Добавьте регрессионный тест, который должен завершиться ошибкой
На этом этапе вы зафиксируете обнаруженный дефект: некорректный JSON должен возвращать предсказуемый ответ 400, но исходный обработчик позволяет исключению разбора выйти наружу.
Добавьте один тест с помощью >>, сохранив два существующих теста. Тело запроса содержит неполный JSON-текст {. Утверждение сначала проверяет статус ответа, а затем разбирает JSON, чтобы причина сбоя однозначно указывала на нарушение HTTP-контракта.
cat >> test/support.test.mjs <<'JS'
test('malformed JSON returns 400 before the upstream', async () => {
const response = await mf.dispatchFetch('http://worker.test/requests', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: '{'
});
assert.equal(response.status, 400);
assert.deepEqual(await response.json(), {error: 'invalid_json'});
assert.equal(calls.length, 0);
});
JS
node --test --test-reporter=spec test/support.test.mjs
Ожидаются три теста: два пройдут, а тест некорректного JSON завершится ошибкой. В утверждении будет указано фактическое значение 500 вместо ожидаемого 400, а процесс завершится с ненулевым кодом. Среда выполнения также может вывести исходное исключение разбора. Это ожидаемый дефект, а не причина менять ожидаемый статус на 500. Ошибка импорта, отсутствие пакета или сбой уже существующего теста указывают на другую проблему.
Изучите незащищённый вызов await request.json() в обработчике. При некорректном JSON ни один запрос не должен достигать фикстуры. Выполните проверку, пока дефект ещё присутствует: этот этап специально проверяет, что регрессионный тест завершается ошибкой, а существующие тесты проходят. На следующем этапе вы исправите реализацию.
Исправьте разбор JSON, не ослабляя тест
На этом этапе вы обработаете только ошибку разбора JSON, сохранив существующую маршрутизацию, проверку входных данных и обработку ошибок внешнего сервиса. Полностью замените обработчик следующей исправленной версией. Блок try/catch вокруг request.json() преобразует исключение синтаксического разбора в JSON-ответ с HTTP-статусом 400. Отдельный блок try/catch для внешнего сервиса по-прежнему обрабатывает сетевые ошибки и ошибки ответа.
cat > src/index.js <<'JS'
export default {
async fetch(request, env) {
const path = new URL(request.url).pathname;
if (path !== '/health' && path !== '/requests') {
return Response.json({error: 'not_found'}, {status: 404});
}
const allowed = path === '/health' ? 'GET' : 'POST';
if (request.method !== allowed) {
return Response.json({error: 'method_not_allowed'}, {
status: 405, headers: {Allow: allowed}
});
}
if (path === '/health') return Response.json({status: 'ok'});
const mediaType = (request.headers.get('content-type') || '').split(';')[0].trim().toLowerCase();
if (mediaType !== 'application/json') {
return Response.json({error: 'unsupported_media_type'}, {status: 415});
}
let body;
try {
body = await request.json();
} catch {
return Response.json({error: 'invalid_json'}, {status: 400});
}
if (!body || Array.isArray(body) || typeof body.subject !== 'string' ||
body.subject.trim().length < 1 || body.subject.trim().length > 80) {
return Response.json({error: 'invalid_subject'}, {status: 422});
}
const subject = body.subject.trim();
try {
const upstream = await fetch(`${env.UPSTREAM_URL}/tickets`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({subject})
});
if (!upstream.ok) {
return Response.json({error: 'upstream_unavailable'}, {status: 502});
}
const ticket = await upstream.json();
return Response.json({ticket: ticket.ticket, subject}, {status: 201});
} catch {
return Response.json({error: 'upstream_unavailable'}, {status: 502});
}
}
};
JS
node --test --test-reporter=spec test/support.test.mjs
Ожидается, что все три теста пройдут, включая неизменённый тест некорректного JSON. Запустите ту же команду ещё раз, чтобы убедиться, что новый процесс тестирования также завершается успешно:
node --test --test-reporter=spec test/support.test.mjs
В обоих запусках должно быть по три успешно пройденных теста и ноль ошибок. Каждый тест получает новую среду выполнения и пустой список вызовов фикстуры. Не отключайте падающий тест и не принимайте ответ 500, чтобы сделать набор тестов успешным. Используйте проверку: она проверяет и тесты учащегося, и отдельный набор ответов среды выполнения.
Расширьте проверку границ и завершите работу локально
На этом этапе вы защититесь ещё от двух регрессий: передачи некорректных входных данных во внешнюю зависимость и выдачи успешного ответа при сбое внешнего сервиса. Добавьте следующие тесты, не удаляя предыдущие три.
Первый тест перебирает некорректные значения JSON и проверяет ответ 422, а затем проверяет передачу текста неподдерживаемого типа и ожидает ответ 415. Ни один из этих запросов не должен обращаться к внешнему сервису. Второй тест начинает с пустой фикстуры, имитирует ответ зависимости 503 и ожидает от API контролируемый JSON-ответ 502. Сравнение полного тела ответа также не позволяет внутренней диагностике фикстуры просочиться наружу.
cat >> test/support.test.mjs <<'JS'
test('invalid subjects and media types never reach the upstream', async () => {
for (const body of [null, [], {}, {subject: 5}, {subject: ' '}, {subject: 'x'.repeat(81)}]) {
const response = await mf.dispatchFetch('http://worker.test/requests', {
method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(body)
});
assert.equal(response.status, 422);
assert.deepEqual(await response.json(), {error: 'invalid_subject'});
}
const response = await mf.dispatchFetch('http://worker.test/requests', {
method: 'POST', headers: {'Content-Type': 'text/plain'}, body: 'hello'
});
assert.equal(response.status, 415);
assert.deepEqual(await response.json(), {error: 'unsupported_media_type'});
assert.equal(calls.length, 0);
});
test('upstream errors are contained with fresh fixture state', async () => {
assert.equal(calls.length, 0);
const response = await mf.dispatchFetch('http://worker.test/requests', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({subject: 'simulate-outage'})
});
assert.equal(response.status, 502);
assert.deepEqual(await response.json(), {error: 'upstream_unavailable'});
assert.deepEqual(calls, [{subject: 'simulate-outage'}]);
});
JS
node --test --test-reporter=spec test/support.test.mjs
Ожидаются пять успешно пройденных тестов и ноль ошибок. Запустите набор ещё раз: значения demo-1 и один записанный вызов при сбое должны оставаться неизменными, поскольку состояние фикстуры сбрасывается перед каждым тестом.
node --test --test-reporter=spec test/support.test.mjs
Вся работа выполнялась локально: тестовые URL передавались в Miniflare, каждый исходящий вызов Worker перехватывался, а каждая среда выполнения удалялась. Убедитесь, что в виртуальной машине не сохранены данные входа в Cloudflare:
npx wrangler whoami --json
Ожидается "loggedIn": false; команда проверки статуса без авторизации может завершиться с ненулевым кодом. Не выполняйте вход в рамках этой лабораторной работы. Удалять облачные ресурсы не нужно. Выполните финальную проверку: она обращается к реальному локальному API и подтверждает, что ваши тесты отклоняют временные неисправные копии и принимают исправленный код. Эти проверочные копии не изменяют ваш проект. После этого завершите работу виртуальной машины.
Локальные тесты среды выполнения делают регрессии воспроизводимыми. Они не проверяют права владения аккаунтом, настройки развёртывания, реальные зависимости в Интернете или поведение раскатки на периферийных узлах; для этого нужны удалённые проверки курса.
Итоги
Вы написали тесты, которые выполняют API в локальной среде выполнения Workers, воспроизвели регрессию «500 вместо 400» и исправили реализацию, не ослабляя ожидаемый контракт. Вы добавили проверки входных данных и ошибок внешнего сервиса, сбрасывали состояние фикстуры между тестами и удаляли каждую среду выполнения. Набор тестов оставался локальным и воспроизводимым, без учётных данных Cloudflare и записи облачных ресурсов.

