Исполняемые спецификации на flang Примеры внутри правила: новый вид предметных unit-тестов
0%

Примеры внутри правила: новый вид предметных unit-тестов

Примеры внутри правила: новый вид предметных unit-тестов

Код в этой главе записан в прежней поверхности языка — со словами категория, объект, утилита. Сегодняшний компилятор её слова читает, но программой такой файл не считает: файл, где есть только утилиты, flang check отклоняет. Разбор задачи в главе верен; синтаксис переносится по таблице из главы «Старые модели».

Пример в FTS — не текст в документации и не отдельный тестовый файл на другом языке. Это часть модели, которая выполняется тем же движком, что и правило, рядом с которым она написана. В этой главе — чем это отличается от обычного unit-теста и почему сервис в этом курсе не поднимается, если пример разошёлся с правилом.

Минимальный пример

категория «Продажи»

  объект Покупка
    сумма является деньгами
    «постоянный клиент» является признаком

  утилита «Рассчитать скидку»
    принимает Покупка
    возвращает деньги
    начинает с 0

    правило «Большая покупка»
      если сумма не меньше 10000
      и сумма не больше 100000
      то добавить 10 процентов от поля сумма

    правило «Постоянный клиент»
      если «постоянный клиент» равен да
      и сумма больше 0
      и сумма не больше 100000
      то добавить 5 процентов от поля сумма

    правило «Очень крупная покупка»
      если сумма больше 100000
      то добавить 15000

    свойство «Скидка ограничена»
      результат не больше 15000

    пример «Обычная покупка»
      дано сумма равна 5000
      дано «постоянный клиент» равен нет
      ожидается результат равен 0

    пример «Постоянный клиент на пять тысяч»
      дано сумма равна 5000
      дано «постоянный клиент» равен да
      ожидается результат равен 250

    пример «Большая покупка постоянного клиента»
      дано сумма равна 20000
      дано «постоянный клиент» равен да
      ожидается результат равен 3000

    пример «Покупка на потолок скидки»
      дано сумма равна 100000
      дано «постоянный клиент» равен да
      ожидается результат равен 15000

    пример «Очень крупная покупка»
      дано сумма равна 200000
      дано «постоянный клиент» равен нет
      ожидается результат равен 15000

Это тот же файл, что использует HTTP-сервис из следующей главы: static/spec/models/order-discount.fts. Пять примеров — не иллюстрация в статье, а пять исполняемых утверждений о правилах выше.

Что делает компилятор

пример разбирается в структуру с входными дано и одним ожидается. Отдельная команда fts test не читает документацию и не парсит комментарии — она находит в модели все примеры всех утилит и для каждого:

  1. подставляет значения дано во вход утилиты;
  2. выполняет evaluateUtility — те же шесть шагов, что и обычный вызов (начинает с, правила по порядку, свойства);
  3. сравнивает фактический результат с ожидается результат равен ... через Object.is, то есть строго, без неявного приведения типов;
  4. если утилита ни разу не завершилась исключением, но результат не совпал — пример считается упавшим; если правило или свойство сами бросили ошибку (например, нарушено свойство) — пример тоже считается упавшим, а причина попадает в отчёт.
fts test static/spec/models/order-discount.fts --pretty

Команда возвращает JSON с числом пройденных и упавших примеров и завершается ненулевым кодом, если хотя бы один не сошёлся, — этого достаточно, чтобы воткнуть её в CI без дополнительной обвязки.

Чем это отличается от unit-теста

Обычный unit-тест живёт в отдельном файле, на языке тестового фреймворка, и использует термины реализации: имя функции, структуру аргументов, мок зависимости. Пример FTS живёт внутри .fts-файла, сразу под правилом, которое проверяет, и использует термины предметной области: «постоянный клиент», «сумма», а не input.amount и input.isLoyal. Аналитик или предметный эксперт может прочитать и предложить пример, не открывая исходный код.

При этом пример — не текстовая иллюстрация вроде docstring с ожидаемым выводом. Он типизирован (компилятор проверяет, что поля в дано существуют в объекте принимает), исполняется интерпретатором при fts test и повторно исполняется в сгенерированном node:test-файле после fts generate. Одна запись — три места проверки одного и того же контракта.

Стартовая проверка вместо позднего провала

examples/spec/discount-api/server.mjs вызывает testUtilities один раз при старте, до открытия сокета:

const document = assertValid(compile(await readFile(modelFile, 'utf8')));
const tests = testUtilities(document);
if (!tests.valid) {
  const failed = tests.results.filter((result) => !result.passed);
  throw new Error(
    `FTS-примеры не прошли (${failed.length} из ${tests.total}): ` +
      failed.map((result) => ${result.example}» ожидалось ${result.expected}, получено ${result.actual}`).join('; '),
  );
}

Если кто-то в pull request поменял правило и не заметил, что старый пример теперь считает иначе, сервис не запустится — не через час эксплуатации, а на первой секунде. server.test.mjs проверяет тот же путь через HTTP: поднимает реальный сервер на случайном порту и обращается к нему через fetch, без моков. Тест сервис не поднимается без прохождения предметных примеров читает /health и сверяет, что examples равно 5/5 — то есть что все пять примеров модели прошли до того, как сервер ответил хоть на один запрос.

Покрытие правил примерами

FTS не требует стопроцентного покрытия автоматически, но структура файла подталкивает к нему: у утилиты с тремя правилами и одним свойством разумно видеть примеры хотя бы на такие случаи — ни одно правило не сработало, каждое правило сработало по отдельности, несколько правил сработали вместе, и (отдельно, не в модели статьи, а в упражнении ниже) случай, ломающий свойство. Ревьюер, который видит новое правило без нового примера, вправе отклонить изменение по той же причине, по которой отклоняют код без теста.

правило «Порог»
  если сумма не меньше 10000
  то добавить 10 процентов от поля сумма

пример «Ровно ниже порога»
  дано сумма равна 9999
  ожидается результат равен 0

пример «Ровно на пороге»
  дано сумма равна 10000
  ожидается результат равен 1000

пример «Выше порога»
  дано сумма равна 10001
  ожидается результат равен 1000.1

Три примера на одно правило — не избыточность, а проверка именно того места, где чаще всего ошибаются: не меньше включает границу, а не исключает её.

Что всё равно тестировать обычным framework

FTS examples закрывают одну конкретную область — детерминированную предметную политику. Они не заменяют:

  • интеграционные тесты базы данных и очередей;
  • HTTP contract tests входа и выхода сервиса;
  • component tests поведения React;
  • E2E пользовательского пути;
  • нагрузочные и security tests.

server.test.mjs показывает границу на практике: тест про /health и число примеров — это всё ещё FTS-компетенция; тест про статус 400 при неверном типе входа — это уже HTTP-контракт, обычный node:test без обращения к testUtilities.

Генерация node:test

fts generate static/spec/models/order-discount.fts --out generated

Появятся fts.utilities.ts и fts.utilities.test.ts — обычный TypeScript и тесты на встроенном node:test, построенные из тех же примеров. В курсе это показано в examples/spec/typescript-codegen: команда --check сравнивает записанный код с тем, что сгенерировала бы модель сейчас, и падает, если кто-то поправил .ts руками мимо .fts. Сгенерированный файл — build artifact, а не второй источник истины.

Практика в песочнице

  1. На вкладке «Примеры» найдите все пять примеров модели и определите, какое правило проверяет каждый из них по отдельности, а какой — их сочетание.
  2. Допишите пример, где сумма равна 5000, а постоянный клиент — нет, но ожидание намеренно указано неверно (например, 100 вместо 0). Переключитесь на вкладку run и убедитесь, что несовпадение видно сразу, а не маскируется.
  3. Уберите у утилиты все примеры и посмотрите, какую диагностику вернёт вкладка — это тот же код, что вернула бы команда fts test в терминале.

Типичные ошибки

  • FTS_NO_UTILITY_EXAMPLES — в модели есть утилита, но ни одного пример. fts test и testUtilities не считают это пустым успехом: без примеров нечего проверять, и команда завершается ошибкой, а не отчётом «0 из 0». Правило без примера — недоделанная утилита, а не готовая.
  • Несовпадение ожидается с фактическим результатом не бросает специальный код: пример просто помечается passed: false в отчёте fts test, с полями expected и actual рядом. Читайте оба значения — часто ошибка не в правиле, а в самом примере, который считали в уме и один раз ошиблись в проценте.
  • Похожий по духу, но не тождественный код — FTS_WITNESS_MISMATCH. Он относится не к примерам утилит, а к теоремам и доказательствам: когда реальные данные в JSON-контексте не совпадают с тем, что заявлено в дано. Эта механика — тема отдельной главы курса про морфизмы и теоремы.

Чек-лист

  • У каждой утилиты есть хотя бы один пример — иначе fts test откажется запускаться.
  • Новое правило сопровождается новым примером в том же pull request.
  • fts test model.fts --pretty встроен в CI и в старт локального сервиса, а не запускается только руками перед релизом.
  • Сгенерированные fts.utilities.test.ts не редактируются вручную — правки вносятся в .fts, затем повторяется fts generate.
  • Пример читается человеком, который не открывал код: имена полей — те же, что в объекте, без сокращений и camelCase.

Кейсы каталога по этой теме

Дальше: Node.js

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов
Дальше