Use Cases zu Tests: von Markdown über JSON zu pytest auf echter Hardware

Embedded Systems Software Agentic Engineering

Der erste Teil dieser Serie hat eine Protokoll-Spezifikation in Markdown beschrieben, die über JSON in vier Zielsprachen generiert wird. Damit liegt die Wahrheit über das Wire Format zentral, und alle Implementierungen halten sich daran. Was noch fehlt, ist der Nachweis, dass die Implementierungen sich auf echter Hardware auch so verhalten, wie die Spec es vorgibt.

Dieser Artikel beschreibt den zweiten Schritt: Use Cases werden ebenfalls in Markdown geschrieben, in dieselbe JSON Zwischenform transformiert und von pytest als Testbench gegen echte Hardware abgefahren. Die Bench läuft auf einem Linux® HIL, mit dem aus SocketCAN auf WSL bekannten Werkzeugkasten für die Vorklärung am Entwicklerlaptop.

Use Cases als Markdown

Use Cases beschreiben das Verhalten aus Anwendersicht: was geschickt wird, was zurückkommt, in welchem Zustand das Gerät vorher und nachher ist, welche Fehler erwartet sind. Sie sind die Brücke zwischen Domänenexperten und Engineering, und sie gehören in eine Form, die beide lesen können. Markdown ist das, mit denselben Konventionen wie die Protokoll Spec: Überschrift pro Use Case, Tabellen für Vor- und Nachbedingungen, Codeblöcke für konkrete Nachrichtenfolgen, freier Text für die Begründung.

Ein typischer Use Case beschreibt einen einzelnen Workflow: “Gerät einschalten, Verbindung aufbauen, Konfiguration lesen, einen Wert ändern, prüfen ob die Änderung persistent ist”. Jeder Schritt referenziert Nachrichten aus der Protokoll Spec, sodass Use Cases nie etwas neu erfinden können, was die Spec nicht kennt.

Der Parser, wieder

Ein zweiter Markdown Parser zieht aus den Use Case Dokumenten eine strukturierte JSON Repräsentation: pro Use Case die Schritte, die erwarteten Antworten, die Vorbedingungen (Geräte Konfiguration), die Wartezeiten, die Erfolgs- und Fehlerkriterien.

Wichtig ist hier dieselbe Disziplin wie beim Protokoll Parser: das JSON ist Build Artefakt, kein bearbeitbares Format. Wer einen Use Case ändert, ändert das Markdown. Der Parser läuft im Build und scheitert, wenn Bezüge auf nicht existierende Nachrichten gemacht werden (er liest dafür auch das Protokoll JSON mit ein, beide Welten passen also automatisch zusammen).

pytest als Testbench Runner

Auf der Test Seite verwenden wir pytest, weil es drei Eigenschaften mitbringt, die für eine Testbench zählen: parametrisierte Tests (ein Test, viele Eingaben), klare Fixtures (Setup und Teardown sauber getrennt), und ein ausgewachsenes Plugin Ökosystem (Reports, Parallelisierung, Hardware Allokation).

Ein Generator erzeugt aus der Use Case JSON eine pytest Suite. Pro Use Case entsteht eine test_* Funktion, die durch die Schritte iteriert und für jeden Schritt die Nachrichten sendet (über die generierten Python Bindings aus Teil 1) und die Antworten gegen die erwarteten Werte vergleicht.

Skelett, wie es typischerweise aussieht:

import pytest
from generated.protocol import Connect, ReadConfig, WriteValue
from bench.fixtures import target

@pytest.mark.usecase("set_persistent_value")
def test_set_persistent_value(target):
    target.send(Connect(role="admin"))
    cfg = target.send_expect(ReadConfig(), reply_type=ReadConfig.Reply)
    target.send(WriteValue(key=42, value=1234))
    target.power_cycle()
    target.send(Connect(role="admin"))
    cfg2 = target.send_expect(ReadConfig(), reply_type=ReadConfig.Reply)
    assert cfg2.values[42] == 1234, "value not persisted across power cycle"

Diese Funktion ist nicht von Hand geschrieben, sondern aus dem Use Case generiert. Sie bleibt aber als Datei im Repo liegen, damit pytest sie diskovern kann und Reviewer sehen, was läuft.

Fixtures: das target

Die target Fixture ist die Brücke zur Hardware. Sie kümmert sich um Allokation eines freien Bench Slots, Setup der seriellen oder Ethernet Verbindung, Reset des Geräts in einen sauberen Ausgangszustand, und um Teardown am Ende. Sie ist die einzige Stelle, die “weiss”, an welcher physikalischen Hardware der Test läuft. Damit lassen sich dieselben Use Case Tests gegen einen lokalen Simulator, einen lokalen USB Adapter oder einen Remote HIL fahren, ohne dass die Tests selbst es merken.

Die Fixture ist auch der Hebel für Parallelisierung. Mehrere Bench Slots, mehrere pytest Worker, die Tests verteilen sich automatisch. Dafür gibt es etablierte Plugins wie pytest-xdist, die mit eigenen Fixtures arbeiten und die Hardware Allokation atomar abfangen.

Der Iterationszyklus

Eine typische Änderung läuft so:

  1. Ein Domänenexperte erweitert einen Use Case, weil ein neuer Workflow abgedeckt werden soll.
  2. Die Markdown Datei wird angepasst, ein neuer Schritt oder ein neuer Use Case wird hinzugefügt.
  3. Der Build läuft, der Parser regeneriert das JSON, der Generator regeneriert die pytest Suite.
  4. Im Pull Request sieht man den Diff der Use Case Markdown, den Diff des JSON, und den Diff der generierten Tests. Drei Sichten auf dieselbe Änderung.
  5. pytest läuft auf dem HIL Bench Slot, das Ergebnis steht im PR.

Wenn ein Test scheitert, gibt es zwei mögliche Wahrheiten: entweder die Firmware tut nicht, was die Spec vorgibt, oder die Spec ist mehrdeutig oder falsch. Beides wird in der Markdown Spec geklärt, nicht in den Tests. Tests werden nie händisch angepasst, damit sie grün werden. Diese Regel ist dieselbe wie in der SSOT Pipeline: die Spec gewinnt, immer.

Was die Agenten dabei tun

Die Generator Seite (Use Case JSON zu pytest Code) ist mechanisch genug, dass sie ein Skript erledigen könnte. Wir lassen sie trotzdem von einem AI Agenten machen, weil derselbe Agent die Use Case Reviews unterstützt: er erkennt fehlende Vorbedingungen, zeigt redundante Tests, schlägt Edge Cases vor, die im Use Case nicht erwähnt sind. Das ist die Stelle, an der ein Agent mehr kann als ein Generator, und genau dort, wo er im Iterationszyklus echten Mehrwert bringt.

Wichtig: die Auswahl, welche Vorschläge übernommen werden, bleibt beim Domänenexperten. Der Agent schreibt nichts in die Spec hinein. Er liefert nur Vorschläge in einem Kommentar Format, das in den Pull Request fliesst.

Stolpersteine, ehrlich

Was übrig bleibt

Ein Markdown Format für Use Cases, einen Parser zu JSON, einen Generator zu pytest, eine Bench Fixture und ein paar Spielregeln. Use Cases werden mit den Domänenexperten verhandelt, nicht mit den Tests. Tests werden generiert, nicht geschrieben. Reviewer sehen drei konsistente Sichten auf jede Änderung: Use Case Markdown, JSON Zwischenform, generierten Test.

Zusammen mit der Protokoll Spec aus Teil 1 schliesst sich der Kreis: dieselbe Quelle treibt die Protokoll Implementierung in allen Sprachen, derselbe Stil treibt die Test Implementierung gegen das echte Verhalten. Spec, Code, Tests, drei Sichten auf dieselbe Wahrheit.

Schlüsselwörter: Spec Driven Development, pytest Testbench Embedded, Use Cases Markdown zu Test, Hardware in the Loop, HIL Bench Setup, Test Automatisierung Embedded, pytest-xdist, SocketCAN Test, Protokoll Tests generieren, Testbench Beratung, Embedded Consulting Schweiz, swissEmbedded Firmware Test

Eine Testbench, die mit den Anforderungen mitwächst, statt hinterherzuhinken? Wir bauen die Pipeline aus Use Case Markdown, JSON Zwischenform und pytest Generator auf, inklusive Bench Fixture und HIL Anbindung.

Kontaktieren Sie uns →