Материал FTS examples: a new kind of subject unit tests
0%

FTS examples: a new kind of subject unit tests

FTS examples: a new kind of subject unit tests

An example in FTS is not text in documentation and not a separate test file in another language. It is a part of the model executed by the same engine that runs the rule, next to which it is written. In this chapter — how this differs from a regular unit test and why the service in this course does not start if the example diverges from the rule.

Minimal example

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

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

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

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

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

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

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

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

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

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

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

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

This is the same file used by the HTTP service in the next chapter: static/fts/models/order-discount.fts. Five examples — not an illustration in the article, but five executable statements about the rules above.

What the compiler does

пример walks through the structure with input дано and one ожидается. A separate fts test team does not read documentation and does not parse comments — it finds in the model all examples of all utilities and for each:

  1. substitutes values дано into the utility’s input;
  2. performs evaluateUtility — the same six steps as a regular call (начинает с, rules in order, properties);
  3. compares the actual result with ожидается результат равен ... via Object.is, that is, strictly, without implicit type coercion;
  4. if the utility never terminated with an exception, but the result did not match — the example is considered failed; if a rule or property itself threw an error (for example, a property was violated) — the example is also considered failed, and the reason is included in the report.
fts test static/fts/models/order-discount.fts --pretty

The command returns JSON with the number of passed and failed examples and exits with a non-zero code if at least one did not match — this is enough to plug it into CI without additional wrapping.

What is the difference from a unit test

A regular unit test lives in a separate file, in the language of the test framework, and uses implementation terms: function name, argument structure, mock dependency. An example FTS lives inside a .fts file, right under the rule that checks it, and uses domain terms: “regular customer”, “amount”, not input.amount and input.isLoyal. An analyst or domain expert can read and suggest an example without opening the source code.

At the same time, an example is not a textual illustration like a docstring with expected output. It is typed (the compiler checks that fields in дано exist in the object принимает), executed by the interpreter during fts test, and re-executed in the generated node:test file after fts generate. One entry — three places to check the same contract.

Early Check Instead of Late Failure

examples/fts/discount-api/server.mjs calls testUtilities once on startup, before opening the socket:

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('; '),
  );
}

If someone in a pull request changed a rule and didn’t notice that the old example now calculates differently, the service won’t start — not after an hour of operation, but on the first second. server.test.mjs checks the same path via HTTP: it runs a real server on a random port and accesses it through fetch, without mocking. Test сервис не поднимается без прохождения предметных примеров reads /health and checks that examples equals 5/5 — that is, that all five model examples completed before the server responded to even one request.

Coverage of rules by examples

FTS does not require automatic 100% coverage, but the file structure pushes toward it: for a utility with three rules and one property, it makes sense to see examples for at least these cases — no rule triggered, each rule triggered individually, several rules triggered together, and (separately, not in the article model, but in the exercise below) a case breaking the property. A reviewer who sees a new rule without a new example is justified in rejecting the change for the same reason changes without tests are rejected.

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

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

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

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

Three examples for one rule — not redundancy, but checking exactly the spot where people most often make mistakes: не меньше includes the boundary, rather than excluding it.

What to test with a regular framework anyway

FTS examples cover one specific area — deterministic subject policy. They do not replace:

  • database and queue integration tests;
  • HTTP contract tests for service input and output;
  • component tests for React behavior;
  • E2E user journey tests;
  • load and security tests.

server.test.mjs shows the boundary in practice: a test about /health and the number of examples is still an FTS competency; a test about the 400 status when the input type is incorrect is already an HTTP contract, a regular node:test without referring to testUtilities.

Generation node:test

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

fts.utilities.ts and fts.utilities.test.ts will appear — regular TypeScript and tests on the built-in node:test, built from the same examples. In the course this is shown in examples/fts/typescript-codegen: the --check team compares the written code with what the model would generate now, and fails if someone changed .ts by hand outside of .fts. The generated file is a build artifact, not a second source of truth.

Practice in the sandbox

  1. On the “Examples” tab, find all five model examples and determine which rule checks each of them individually, and which one checks their combination.
  2. Add an example where the sum equals 5000, and the regular customer is нет, but the expected result is intentionally incorrect (e.g., 100 instead of 0). Switch to tab run and make sure the mismatch is immediately visible, not masked.
  3. Remove all examples from the utility and see what diagnostics the tab returns — it is the same code that the fts test command would return in the terminal.

Common Fallacies

  • FTS_NO_UTILITY_EXAMPLES — the model has a utility, but no пример. fts test and testUtilities do not consider this an empty success: without examples there is nothing to check, and the team ends with an error, not a report “0 out of 0”. A rule without examples is an unfinished utility, not a ready one.
  • A mismatch between ожидается and the actual result does not throw a special code: the example is simply marked passed: false in the report fts test, with fields expected and actual nearby. Read both values — often the error is not in the rule, but in the example itself, which was calculated in mind and once made a mistake in percentage.
  • A similar in spirit, but not identical code — FTS_WITNESS_MISMATCH. It relates not to utility examples, but to theorems and proofs: when real data in JSON context do not match what is declared in дано. This mechanism is the subject of a separate chapter of the course on morphisms and theorems.

Checklist

  • Each utility has at least one example — otherwise fts test will refuse to run.
  • A new rule is accompanied by a new example in the same pull request.
  • fts test model.fts --pretty is integrated into CI and into the local service startup, not run manually only before a release.
  • Generated fts.utilities.test.ts are not edited manually — changes are made in .fts, then fts generate is repeated.
  • The example is read by someone who hasn’t opened the code: field names — the same, as in the object, without abbreviations and camelCase.

Cases in the catalog on this topic

Next: Node.js

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

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

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

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