Skip to content

Quality Assurance and Testing ​

A transformation that produces wrong output is worse than one that crashes — because wrong output travels downstream silently. The order with a missing VAT field gets invoiced without tax. The patient record with a swapped first/last name enters the hospital system. The payment with a wrong decimal position moves 100x the intended amount.

UTL-X's testing strategy is designed to catch these errors before they reach production, with five layers from unit tests to black-box conformance testing.

The Five Testing Layers ​

UTL-X uses layered testing — each layer catches different classes of bugs:

Layer 5: Conformance Suite (Python, 500+ tests)
  └── Layer 4: Integration Tests (Kotlin)
       └── Layer 3: Module Tests (Kotlin)
            └── Layer 2: Format Tests (Kotlin)
                 └── Layer 1: Unit Tests (Kotlin)

Each outer layer depends on the inner layers passing. If a unit test fails, everything above it is suspect.

Layer 1: Unit Tests ​

Test individual functions and classes in isolation:

  • UDM type construction, access, and mutation

  • Scalar operations (asString, asNumber, asBoolean, type coercion)

  • Parser edge cases (special characters, Unicode, empty input, deeply nested structures)

  • Each stdlib function (652 functions, each with at least one test)

bash
./gradlew :modules:core:test

These run in milliseconds and catch logic errors in individual components. If toNumber("1.234,56") returns the wrong value, a unit test catches it instantly.

Layer 2: Format Tests ​

Test each format parser and serializer independently. Every format module has its own test suite:

  • JSON: parser (BOM, precision, special characters), serializer (pretty/compact, writeAttributes)

  • XML: parser (namespaces, attributes, CDATA, encoding), serializer (element ordering, escaping)

  • CSV: parser (delimiters, quoting, headers, regional numbers), serializer (regional formats, BOM output)

  • YAML: parser (anchors, aliases, multi-document, timestamps), serializer (block/flow style)

  • OData: parser (annotation extraction, collection unwrapping), serializer (metadata levels)

  • Schema formats: XSD (pattern detection), JSON Schema (draft versions), Avro, Protobuf, EDMX, Table Schema — each with parse/serialize/round-trip tests

bash
./gradlew :formats:json:test    # one format
./gradlew :formats:test          # all formats

These catch format-specific regressions. When the XML _text serialization leak was fixed, format tests caught the behavior change immediately.

Layer 3: Module Tests ​

Test cross-cutting concerns within a module:

  • Core: interpreter tests (expression evaluation, function calls, let bindings, closures), type system tests, parser tests (grammar edge cases)

  • CLI: command parsing, expression mode (-e), identity mode (format flip), multi-input handling

  • Engine: strategy tests (TEMPLATE, COPY, COMPILED produce identical results), worker pool tests, back-pressure tests

  • Daemon: LSP protocol tests, live preview tests

bash
./gradlew :modules:cli:test
./gradlew :modules:engine:test

These catch interaction bugs — where two components work individually but fail together.

Layer 4: Integration Tests ​

Test end-to-end flows through multiple modules:

  • Input parsing → transformation → output serialization (the full pipeline)

  • Multi-format tests: XML in → JSON out, CSV in → YAML out, OData in → XML out

  • Schema validation integration: load schema → validate → transform → validate

  • Multi-input tests: two inputs with different formats → merged output

  • UTLXe transport tests: HTTP endpoint, gRPC, stdio-proto — same transformation, same result

bash
./gradlew test    # all modules

These catch end-to-end regressions — the parser produces valid UDM, the interpreter evaluates correctly, but the serializer mishandles a specific UDM shape.

Layer 5: Conformance Suite ​

The most important testing layer. The conformance suite is an external test runner (Python) that tests the CLI as a black box — exactly like a user would.

Why Python, Not Kotlin? ​

The conformance suite deliberately runs outside the JVM:

  • Tests the binary (native or JAR) — the actual artifact users install

  • No internal access — tests what the user sees, not internal state

  • Language-independent — could test a future WASM or Rust implementation of UTL-X

  • Easy to extend — add a YAML file, no compilation needed

Test Format ​

Each test is a YAML file with input, transformation, and expected output:

yaml
name: "xml_to_json_passthrough"
input:
  format: xml
  data: "<Order><Customer>Alice</Customer></Order>"
transformation: |
  %utlx 1.0
  input xml
  output json
  ---
  $input
expected:
  format: json
  data: '{"Order":{"Customer":"Alice"}}'

500+ tests cover:

  • Formats: every format pair (XML→JSON, JSON→CSV, YAML→XML, etc.)

  • Stdlib: function behavior (string operations, math, date/time, arrays, objects)

  • Examples: real-world transformations (orders, invoices, IDoc, FHIR)

  • Multi-input: named inputs from different formats

  • Error handling: parse errors, validation failures, edge cases

  • Schema: XSD, JSON Schema, Avro, Protobuf parse/serialize/round-trip

  • Regional: CSV with European, French, Swiss number formatting

Running the Suite ​

bash
cd conformance-suite
python3 utlx/runners/cli-runner/simple-runner.py

Output shows pass/fail per test with details on failures:

Running 470 tests...
[PASS] xml_to_json_passthrough
[PASS] csv_orders_grouping_semicolon
[FAIL] json_to_csv_regional_french
  Expected: "1 234,56"
  Got:      "1234.56"
...
467/470 passed, 3 failed

Auto-Capture: Recording Tests from Usage ​

UTL-X can automatically record test cases from real CLI usage:

bash
utlx --capture transform order-transform.utlx order.xml

This runs the transformation normally AND saves a conformance test YAML file with the input, transformation, and actual output. The test is saved to conformance-suite/utlx/tests/auto-captured/.

Warning: auto-captured tests cement current behavior. They validate what IS, not what SHOULD BE. If the output has a bug, the test captures the bug as "expected" — and the test passes, hiding the bug. Always review auto-captured tests manually before trusting them. This was learned the hard way: auto-captured tests had _text in expected output, which cemented the bug as "correct" behavior.

Auto-capture is valuable for:

  • Building regression tests from production data (sanitized)

  • Documenting existing behavior before refactoring

  • Onboarding: capture a senior developer's transformations as test cases

UTLXe Engine Tests ​

The engine has its own conformance suite testing production-specific behavior:

  • Transport parity: same test against HTTP, gRPC, and stdio-proto — must produce identical results

  • Strategy parity: same test against TEMPLATE, COPY, and COMPILED — must produce identical results

  • Throughput tests: verify 86K+ messages/second under sustained load

  • Batch tests: 100+ instances per schema to catch edge cases in real data

  • Validation tests: pre/post validation with all 7 schema validators

Transport parity and strategy parity are the most important engine tests. If COMPILED produces a different result than TEMPLATE for the same transformation, it's a bytecode generation bug — and these are the hardest bugs to find without automated testing.

Test-Driven Transformation Development ​

For production transformations, write the test first:

  1. Define the contract: write the expected output for a representative input

  2. Write the transformation: make the test pass

  3. Add edge cases: null fields, empty arrays, missing properties, wrong types

  4. Add error cases: invalid input, schema violations, boundary values

  5. Run the conformance suite: verify no regressions in other transformations

yaml
# Step 1: Write this test file FIRST
name: "dynamics365_order_to_ubl_invoice"
input:
  format: json
  data: '{"ordernumber": "ORD-001", "totalamount": 299.99, ...}'
transformation: |
  %utlx 1.0
  input odata
  output xml
  ---
  ...
expected:
  format: xml
  data: '<Invoice xmlns="urn:oasis:..."><cbc:ID>ORD-001</cbc:ID>...</Invoice>'

Then write the .utlx file to make it pass. This inverts the common "write code, then test" pattern — the test IS the specification.

Continuous Integration ​

UTL-X uses GitHub Actions for automated testing on every push and PR:

CLI Pipeline (cli-ci.yml) ​

  1. Build JAR (Kotlin/Gradle)

  2. Run Kotlin tests (layers 1-4)

  3. Build native binary (GraalVM)

  4. Run conformance suite against native binary (layer 5)

  5. Performance benchmark (detect regressions)

Release Pipeline (release.yml) ​

  1. All CI steps above

  2. Build native binaries for 3 platforms (macOS, Linux, Windows)

  3. Build Docker image

  4. Run conformance suite against Docker image

  5. Publish artifacts (GitHub Releases, Homebrew, Chocolatey, Docker registry)

Pull Request Checks ​

Every PR must pass:

  • All Kotlin tests (layers 1-4)

  • Full conformance suite (layer 5)

  • No new test failures (regressions blocked)

This ensures that no code reaches the main branch without passing 500+ tests across all formats, functions, and patterns.

Released under AGPL-3.0.