Embedded Unit Testing (Spezifikationsbasiert)
Erzeugt eine Unit-Test-Suite für eine Reihe von .c/.h-Dateien, die der Benutzer übergibt, unter Verwendung der 7 Schritte des spezifikationsbasierten Testens von Maurício Aniche (Effective Software Testing, Kap. 2). Die Besonderheit dieser Fähigkeit: gute Partitionierung der Domäne jeder Funktion (der wertvollste Schritt), Ableiten von Grenzen, Entwerfen der kleinsten Suite, die dennoch Fehler findet, Automatisierung im richtigen Framework des Projekts und Iteration, bis die Abdeckung das Ziel erreicht.
Lies references/specification-based-testing.md vor Phase 3 – es ist die Kalibrierung dafür, wie man über Partitionen und Grenzen denkt, und die Pragmatik, die eine aufgeblähte oder spröde Suite vermeidet.
Scope Invarianten (zuerst lesen)
- Ausgabe auf Englisch. Alles, was diese Fähigkeit produziert, ist auf Englisch, selbst wenn der Benutzer in einer anderen Sprache schreibt: Testcode, Testnamen, Kommentare, Katalog,
Covers:-Annotationen, Skip/Ignore-Nachrichten, die Pfaddeklaration und die abschließende Zusammenfassung. - Nur Unit-Tests. Erzeuge niemals Integrationstests, E2E-Tests, Systemtests, HIL- oder Robot-Framework-Tests. Die Granularität ist die isolierte Funktion / das isolierte Modul, mit gefakten/gemockten Abhängigkeiten.
- Nur die angegebenen Dateien. Die Suite ist für die
.c/.h-Menge, die der Benutzer übergeben hat. Scanne nicht das Projekt nach anderen Zielen und teste nicht eigenständig benachbarte Module. - Ändere niemals die Zieldateien – nicht vorübergehend, nicht „um zu testen und rückgängig zu machen“. Die Suite kompiliert die originalen
.c-Dateien, ohne Kopie im Testverzeichnis. Um etwas Schwieriges zu erreichen, verwende die öffentliche API, Fakes der Abhängigkeiten oder ein separates Test-Binary. - Autonom. Generiere, führe aus und berichte, ohne um Bestätigung zur Ausführung zu bitten. Es gibt zwei Ausnahmen, bei denen du anhältst und fragst: (a) Pfad C (weder Zephyr noch ESP-IDF), wenn du kein Unit-Test-Framework identifizieren kannst (Phase 0); (b) alle 2 Coverage-Iterationen, ohne das Ziel zu erreichen (Phase 8).
- Kein von dir geschriebener Bericht. Du schreibst keinen Ergebnisbericht. Führe die Tools aus und weise auf die Artefakte hin, die sie erzeugen (Coverage-HTML, Runner-Ausgabe). Das „Ergebnis“ ist das, was das Tool ausgibt, nicht dein Text.
- Coverage-Ziel: Funktion ≥ 95 %, Zeile ≥ 95 %, Zweig ≥ 90 % (95/95/90) standardmäßig. Sobald das Ziel erreicht ist, stoppe die Iteration und liste die verbleibenden nicht abgedeckten Zeilen/Zweige auf. Gehe auf 100/100/100 nur, wenn der Benutzer danach fragt. Unter dem Ziel nur, wenn das Fehlende nachweislich unerreichbar ist (Log/Assert-Rauschen, Testbarkeitseinschränkung des Moduls), wobei jede verbleibende Zeile/jeder verbleibende Zweig begründet wird.
Der Ablauf (Aniches 7 Schritte → 9 Phasen)
Die 7 Schritte sind keine starre Pipeline – sie sind iterativ (Phase 3 enthüllt oft etwas, das dich zurück zu Phase 1 schickt). Die folgenden Phasen sind das Rückgrat.
| Phase | Aniche Schritt | Was du tust |
|---|---|---|
| 0 | — | Umgebung erkennen → Pfad A (Zephyr), B (ESP-IDF) oder C (andere) |
| 1 | 1 Verstehen | Anforderung, Eingaben, Ausgaben jeder gegebenen Funktion |
| 2 | 2 Code erkunden | Abhängigkeiten kartieren, um mit Fakes/Mocks zu isolieren |
| 3 | 3 Partitionen | Nummerierter Katalog von Partitionen (der wichtigste Schritt) |
| 4 | 4 Grenzen | Nummerierter Katalog von Grenzen (On-/Off-Point) |
| 5 | 5 Fälle entwerfen | Partitionen/Grenzen kombinieren, Kombinationen reduzieren |
| 6 | 6 Automatisieren | Suite im Framework des Pfades schreiben, mit Rückverfolgbarkeit |
| 7 | — | Tools ausführen: Ausführung + Coverage |
| 8 | 7 Erweitern | Über Lücken iterieren (Coverage + Katalogeinträge ohne Test) |
Phase 0 — Umgebungserkennung
Identifiziere die Umgebung des Projekts, das die angegebenen Dateien enthält, in dieser Reihenfolge, und wähle den Pfad. Robot Framework, HIL, Integrationstests mit pytest sind KEINE Unit-Test-Frameworks – sie zählen nie in irgendeinem Pfad.
-
Zephyr? → Pfad A. Signale:
west.yml/west.yaml;CMakeLists.txtmitfind_package(Zephyr ...);prj.conf;testcase.yaml/sample.yaml;tests/;ztest/ZTEST_SUITE. Tooling: twister + ztest + FFF + Coverage. Liesreferences/zephyr-path.md. -
ESP-IDF? → Pfad B. Signale:
CMakeLists.txtmitidf_component_register(;sdkconfig/sdkconfig.defaults; Verwendung von$ENV{IDF_PATH}/idf.py; einemain/-Komponente. Tooling: ESP-IDFs natives Unity (Host-Build,linux-Ziel) + gcov/gcovr. Verwende Unity direkt – halte nicht an, um zu fragen. Ausnahme: wenn das Projekt bereits konsistent ein anderes Unit-Test-Framework verwendet (Ceedling, GoogleTest…), folge diesem. Liesreferences/espidf-path.md. -
Andere (weder Zephyr noch ESP-IDF) → Pfad C. Untersuche die vorhandene Unit-Test-Struktur (Unity, CMock, Ceedling, GoogleTest, benutzerdefiniert) und folge dem Muster des Projekts getreu. Wenn kein Unit-Test-Framework gefunden wird, HALTE AN und frage den Benutzer, welches verwendet werden soll – nimm niemals einen stillen Standard in Pfad C an. Lies
references/non-zephyr-path.md.
Gib im Antworttext an (nicht nur in deiner Argumentation), bevor du fortfährst, den gewählten Pfad und die konkreten Signale, die ihn rechtfertigen, mit Datei und was du gefunden hast – z. B. „Pfad A (Zephyr): zephyr/west.yml, find_package(Zephyr) in tests/.../CMakeLists.txt, ZTEST_SUITE-Suites in tests/lib/.“
Phase 1 — Anforderung, Eingaben und Ausgaben verstehen
Für jede Funktion, die die Suite abdecken wird, extrahiere durch Lesen des .c/.h:
- die Geschäftsregel: was sie tut und was sie nicht tun darf (Ecken eingeschlossen);
- die Eingaben: Typen, Bereich/Domäne, Einschränkungen zwischen Variablen;
- die Ausgaben: wie Eingaben zum Ergebnis werden (und beobachtbare Seiteneffekte: Ausgabe geschrieben, Fehler zurückgegeben, Bewegung ausgegeben/nicht ausgegeben).
Ein Teil davon ist explizit; ein Teil sind implizite Domänenregeln (Sättigung, Überlauf, Einheiten, Vorzeichen, Spiel), die du ableitest. Diese impliziten Regeln sind es, wo sich Fehler verstecken.
Phase 2 — Den Code erkunden
Lese das gesamte .c/.h und erstelle die Karte der zu isolierenden Abhängigkeiten: HAL, Treiber, Kernel/RTOS, I/O, Zeit, Aufrufe in andere Module. Jede Abhängigkeit wird in Phase 6 zu einem Fake/Mock. Identifiziere auch, wie die eigentliche Logik erreicht wird (direkte öffentliche API oder static-Funktionen über Handle/vtable – rufe sie über die echte API auf, erfinde niemals eine Signatur).
Phase 3 — Identifiziere die Partitionen (der wichtigste Schritt)
Wende die 3 Linsen aus references/specification-based-testing.md auf jede Funktion an: jede Eingabe isoliert, Eingaben in Kombination und Ausgabeklassen. Berücksichtige NULL/leer und die impliziten Domänenregeln. Wenn die Funktion einen Zustand hat, partitioniere auch nach dem Zustand des Moduls/Kontexts.
Erstelle pro Funktion einen benannten, nummerierten Katalog mit stabilen IDs innerhalb der Suite:
- Partitionen:
<func>/P1, P2, … - Grenzen (werden in Phase 4 ausgefüllt):
<func>/B1, B2, …
Jeder Eintrag erhält eine kurze Beschreibung. Dieser Katalog ist der Rückverfolgbarkeitsvertrag der Suite (materialisiert in Phase 6) und die Quelle der Wahrheit für Lücken (Phase 8). Eine gut gemachte Phase 3 sollte beim ersten Coverage-Durchlauf fast alles abdecken.
Phase 4 — Analysiere die Grenzen
Für jede Grenze zwischen benachbarten Partitionen füge zum Katalog den On-Point (den Wert in der Bedingung) und den Off-Point (den nächsten Nachbarn in der gegenüberliegenden Partition) hinzu; Gleichheiten ergeben einen On- und zwei Off-Points. Lasse eine Grenze weg, die bereits durch einen anderen Fall abgedeckt ist – dupliziere nicht. „Partition oder Grenze? Es spielt keine Rolle“: was zählt, ist, dass der Fall existiert.
Phase 5 — Entwerfe die Testfälle
Kombiniere die Partitionen über Eingabekategorien hinweg, aber reduziere die kombinatorische Explosion: teste das Exceptionelle (NULL/leer) einmal und kombiniere es nicht mit allem; verwende die Domäne, um zu kürzen. Verwende eine einzelne Starteingabe, variiere nur das Minimum pro Fall (erleichtert das Debugging). Bei Zweifeln die einfachste Eingabe. Für Eingaben, die nicht am Verhalten teilnehmen, vernünftige/realistische Werte. Wenn ein Fall das gleiche Grundgerüst wie mehrere andere hat, plane ihn als parametrisiert. Liste die Fälle auf und für jeden die Katalog-IDs, die er übt.
Phase 6 — Automatisieren (mit obligatorischer Rückverfolgbarkeit)
Schreibe die Suite im Framework des erkannten Pfades (Syntax, Isolation und Gerüst Details in references/zephyr-path.md, references/espidf-path.md oder references/non-zephyr-path.md).
Der Katalog aus Phase 3/4 wird zu Kommentaren in der Suite, auf zwei Ebenen:
- Pro-Funktion-Header – ein Block am Anfang des Testabschnitts dieser Funktion, der den vollständigen Katalog auflistet (jede Partition/Grenze mit ihrer ID und Kurzbeschreibung).
- Pro-Test-Annotation – jeder Test verweist darauf, welche(n) Eintrag(e) er übt, z. B.
/* Covers: P3 + B1 */.
Obligatorische Regeln (das macht die Rückverfolgbarkeit wertvoll):
- Kein verwaister Test: jeder Test verweist auf ≥ 1 Katalogeintrag.
- Kein verwaister Eintrag: jede Partition/Grenze wird von ≥ 1 Test geübt. Ein Eintrag ohne Test wird als Lücke markiert (speist Phase 8).
- Stabile, identische IDs zwischen Header und Annotationen – damit ein
grep "P4"den Header und jeden Test auflistet, der diese Partition berührt. - Ein Test, der eine Kombination abdeckt, listet alle IDs auf (
/* Covers: P2 + P5 */). Covers:sagt die Wahrheit: jede annotierte ID stimmt mit dem überein, was der Fall tatsächlich übt (Eingabe, Zustand und Assert), nicht nur mit einem Eintrag, der im Katalog existiert. Vor Phase 7, lies die Annotationen gegen jeden Testkörper erneut und korrigiere diejenigen, die nicht übereinstimmen.- In Pfad B und C das gleiche Schema, angepasst an die Syntax des Frameworks (Kommentar über jedem
TEST(...)/TEST_CASE(...)/TEST_F(...)).
Kanonische Form (Pfad A, ztest):
/*
* Katalog — motor_ctrl_transform (Phase 3 · spezifikationsbasiertes Testen)
* Partitionen:
* P1 p_axis_values == NULL -> abgelehnt, Ausgabe nicht geschrieben
* P2 gültige axis_count (1..N) -> transformiert
* P3 positiver Wert -> positive Schritte
* P4 negativer Wert -> Vorzeichen erhalten
* P5 Nullwert -> null Schritte
* Grenzen:
* B1 axis_count == N (max) -> akzeptiert
* B2 axis_count == N+1 -> abgelehnt
*/
/* Covers: P2 + P3 */
ZTEST(motor_ctrl_suite, test_transform_positive_gives_positive_steps) { /* ... */ }
/* Covers: P1 */
ZTEST(motor_ctrl_suite, test_transform_null_input_is_rejected) { /* ... */ }
Hinweis: In neuerem Zephyr erfordert der Runner, dass Testnamen mit
test_beginnen (ZTEST(suite, test_...)) – ohne dieses Präfix sammelt twister den Fall nicht.
Defekt gefunden: Code weicht von der Dokumentation ab
Wenn der Code dem Header, dem Funktionskommentar oder dem API-Vertragsdokument widerspricht (z. B. gibt einen anderen Fehlercode zurück als dokumentiert, stellt den Zustand bei Fehler nicht wieder her), lasse die Suite nicht rot und verwende nicht ZTEST_EXPECT_FAIL. Schreibe zwei Tests:
- Dokumentiertes Verhalten, mit Grund ignoriert – der Assert dessen, was die Dokumentation verspricht, übersprungen mit dem Grund:
ztest_test_skip()(Pfad A) oderTEST_IGNORE_MESSAGE("BUG: ...")(Pfad B). Kommentar darüber:/* Covers: Pn — BUG: <docs say X; code does Y> */. - Charakterisierung – fixiert das aktuelle Verhalten, sodass jede Änderung (einschließlich der Korrektur) sichtbar wird. Kommentar:
/* Covers: Pn — Charakterisierung des obigen BUGs */.
Die Suite endet grün, der Fehler wird in jedem Durchlauf sichtbar (als Skip/Ignore) und das Binary geht in die Messungen ein. Syntax in den Pfad-Dateien. Liste die Defekte in der abschließenden Zusammenfassung auf.
Phase 7 — Ausführung + Coverage
Vor dem Ausführen aktiviere die Projektumgebung. Das Test-Tool ist selten im globalen PATH – es lebt normalerweise in einem Repo-Virtualenv. Ziehe nicht den Schluss „kann nicht ausgeführt werden“, bevor du die Umgebung gesucht und aktiviert hast (dies gilt für jeden Pfad):
- Suche nach einem venv im Firmware-Root (
.venv/,venv/,env/) und verwende sein Binary (.venv/bin/west,.venv/bin/python) odersource .venv/bin/activate. Das venv ist oft ein Symlink: Überprüfe mittest -e .venv/bin/activateoderls -laL(welche dem Link folgen), niemals nur mitls -d/find -type d. Sage, was du gefunden hast: den verwendeten venv-Pfad oder „kein venv in<geprüften Verzeichnissen>“. - Zephyr: setze
ZEPHYR_BASE(z. B.deps/zephyr, oderwest topdir/.west/config) und für den HostZEPHYR_TOOLCHAIN_VARIANT=host. Wennwestnicht imPATHist, aber der Zephyr-Baum existiert, führe twister direkt aus:python3 $ZEPHYR_BASE/scripts/twister .... - ESP-IDF: stelle sicher, dass die IDF-Umgebung gesetzt ist (
. $IDF_PATH/export.shwenn vorhanden) für den Host-Build; andernfalls kompiliere direkt mitgcc(sieheespidf-path.md).
Erst nachdem diese Suche erschöpft ist, ist es ehrlich zu berichten, dass die Ausführung hier nicht möglich war.
Kontextökonomie (Firmware-Builds sind ausführlich und jede Runde liest alles neu):
- Leite Build-, Twister- und gcovr-Ausgabe in eine Datei um (
> build.log 2>&1) und zeige nur die Zusammenfassung (tail -n 30 build.log,grep -E 'error|FAIL|PASS|lines|branches' build.log). Öffne das gesamte Log nur, wenn die Zusammenfassung den Fehler nicht erklärt. - Schreibe jede Testdatei vollständig auf einmal; korrigiere mit gezielten Änderungen.
- Gruppiere unabhängige Lese- und Prüfvorgänge in derselben Runde; lese keine Dateien erneut, die du bereits gelesen hast.
Führe das Tool des Pfades aus und weise auf die Artefakte hin, die es erzeugt (schreibe keinen Bericht):
- Pfad A (Zephyr):
west twister --coverage -p native_sim -T <suite_path>. Artefakte:twister-out/coverage/index.html,twister-out/coverage.json,twister-out/twister.xml,twister-out/twister.json. - Pfad B (ESP-IDF): Host-Build mit Unity +
gcov/gcovr(sieheespidf-path.md); weise auf den Coverage-Bericht (coverage.html/.xml) und die Testausgabe hin. - Pfad C (andere): Der eigene Runner des Projekts + Coverage (z. B.
ceedling gcov, odergcov/gcovrüber den Test-Build). Behalte und weise auf die erzeugten Artefakte hin.
Details und Fehlerbehebung in der Datei jedes Pfades.
Phase 8 — Coverage-Iteration
Vergleiche die von den Tools gemeldete Coverage mit dem 95/95/90-Ziel (oder dem vom Benutzer gewünschten). Unter dem Ziel ist jede erreichbare Zeile/jeder erreichbare Zweig ohne Test eine Lücke; sobald das Ziel erreicht ist, stoppe und liste auf, was nicht abgedeckt bleibt. Füge für jede Lücke Fälle hinzu, die beide Quellen verwenden: die numerische Coverage (rote Zeilen/Zweige) und den Katalog (Partitionen/Grenzen ohne Test). Jede 2 Coverage-Iterationen ohne Erreichen des Ziels: HALTE AN und frage den Benutzer, ob er weitermachen soll.