IoT & Vernetzte Systeme

Geerbter IoT-Code macht Testtipps von 2024 riskant

Du übernimmst eine IoT-Codebasis ohne Tests, aber mit Geräten im Feld. Der naheliegende Plan lautet: Unit-Tests nachrüsten, Testabdeckung erhöhen, später Hardware anschließen. Ich würde die Reihenfolge umdrehen. Zwischen 2024 und 2026 sind Software-Lieferkette und Sicherheitsnachweise dringlicher geworden; ein grüner Build sagt weiterhin wenig darüber aus, was nach einem Verbindungsabbruch oder Update geschieht. Prüfe zuerst die Zustandswechsel, bei denen aus einer Nachricht ein dauerhafter Effekt wird.

Seit 2024 ist ein grüner Build ein noch schwächerer Auslieferungsnachweis

Die EU-Verordnung über Cyberresilienz, der Cyber Resilience Act (CRA), trat im Dezember 2024 in Kraft. Ihre allgemeinen Pflichten gelten erst ab Dezember 2027, während Meldepflichten für bestimmte aktiv ausgenutzte Schwachstellen bereits seit September 2026 greifen. Nicht jede geerbte Anwendung fällt automatisch darunter. Falls euer Produkt betroffen ist, reicht die alte Auskunft „CI war grün“ aber als Antwort auf Fragen nach betroffenen Versionen, Updates und bekannten Fehlern nicht aus: Ein Testergebnis ohne Bezug zur ausgelieferten Firmware lässt sich keinem Gerät zuordnen.

Beginne deshalb mit einer Bestandsaufnahme, die Tests überhaupt erst aussagekräftig macht. Welche Firmware-Version läuft auf welchem Board? Welches Container-Image verarbeitet seine Nachrichten? Wo liegt die Konfiguration für den Eclipse-Mosquitto-Broker, und verwendet der Client MQTT 3.1.1 oder MQTT 5? Prüfe außerdem, ob ein OTA-Update den Gerätezustand oder das Nachrichtenschema verändert. Diese Fragen wirken administrativ, bestimmen aber die Testgrenze: Ein bestandener Test gegen die heutige Serverversion belegt nichts für ein Gerät, das noch ein älteres Payload-Format sendet.

Die 2024 veröffentlichten Formate SPDX 3.0 und CycloneDX 1.6 geben Teams präzisere Möglichkeiten, Komponenten zu dokumentieren. Eine SBOM ersetzt allerdings keinen Test, weil sie weder einen verlorenen MQTT-ACK noch eine doppelt verbuchte Nachricht erkennt. Ich würde auch nicht zuerst eine vollständige SBOM-Pipeline aufbauen, nur um „Compliance erledigt“ zu melden: Ohne Zuordnung zwischen Artefakt, Gerätekonfiguration und beobachtetem Verhalten produziert sie vor allem einen gut formatierten blinden Fleck. Halte stattdessen für jeden kritischen Test Firmware-Hash, Backend-Commit und Broker-Konfiguration fest. Das ist weniger glamourös, ermöglicht aber eine belastbare Fehlerreproduktion.

Die These zur Board-Pflege – Warum dein erstes IoT Board mehr Pflege als Code frisst – beschreibt einen realen Aufwand. Für die übernehmende QA ist dennoch nicht der Zustand des Boards die erste Priorität, sondern die Frage, ob nach einem Neustart dieselbe Aktion erneut ausgeführt wird. Ein gepflegtes Testgerät kann diesen Fehler zuverlässig reproduzieren; verhindern muss ihn die Software.

Der erste Test gehört an die Grenze zwischen Nachricht und Wirkung

Bei MQTT mit QoS 1 kann eine Nachricht erneut eintreffen, weil die Zustellung mindestens einmal erfolgt und eine Bestätigung verloren gehen kann. Der verbreitete Rat „Stell QoS höher, dann ist die Zustellung sicher“ ist für Geschäftseffekte veraltet, sofern „sicher“ als „genau einmal verarbeitet“ verstanden wird: Das Protokoll regelt die Übertragung, nicht die einmalige Buchung in deiner Datenbank. Selbst QoS 2 ersetzt keine Idempotenz, wenn ein Dienst nach dem Schreiben, aber vor seiner eigenen Bestätigung abstürzt.

Suche in der geerbten Codebasis die Stelle, an der aus einem empfangenen Payload ein dauerhafter Eintrag, Alarm oder Steuerbefehl wird. Dort gehört ein erster Charakterisierungstest hin. Halte zunächst das bestehende Verhalten fest, auch wenn es falsch ist; sonst kannst du später nicht zeigen, welchen Fehler die Änderung beseitigt. Für eine eindeutige Ereigniskennung kann etwa die Kombination aus Geräte-ID, Startkennung und Sequenznummer dienen. Die Startkennung ist nötig, wenn ein Gerät seine Sequenznummer nach einem Neustart zurücksetzt.

Dieses mit Python 3 ausführbare Minimalbeispiel zeigt nur die Datenbankgrenze, nicht den gesamten MQTT-Pfad. Speichere es als test_event.py und starte es mit python3 test_event.py:

import sqlite3
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE events (device TEXT, boot TEXT, seq INTEGER, PRIMARY KEY(device, boot, seq))")
def accept(device, boot, seq):
    before = db.total_changes
    db.execute("INSERT OR IGNORE INTO events VALUES (?, ?, ?)", (device, boot, seq))
    return db.total_changes > before
assert accept("board-7", "start-a", 42)
assert not accept("board-7", "start-a", 42)
assert accept("board-7", "start-b", 42)
assert db.execute("SELECT COUNT(*) FROM events").fetchone()[0] == 2
print("Wiederholung erkannt")

Beim Ausführen liefert die Abfrage 2 Einträge; das ist das gemessene Ergebnis dieses Minimaltests, kein Messwert aus einem Feldgerät. Übertrage das Muster nicht blind: Eine vom Gerät frei wählbare Startkennung kann kollidieren oder manipuliert werden. Erkläre im Test deshalb ausdrücklich, wer die Kennung erzeugt, wann sie wechselt und ob die Sequenznummer persistent ist. Wenn die Anwendung neben dem Datenbankeintrag auch einen externen Befehl ausführt, brauchst du zusätzlich einen Test für die Lücke zwischen Commit und Befehl; eine eindeutige Datenbankzeile allein macht zwei getrennte Systeme nicht atomar.

Als veränderbaren Anfangswert würde ich einen Wiederholungsversuch nach 30 Sekunden in den Test aufnehmen, weil er einen Reconnect-Fall leicht reproduzierbar macht. Die Frist ist kein MQTT-Standard und sollte zur tatsächlichen Retry-Konfiguration passen. Noch wichtiger als die Wartezeit ist die Behauptung des Tests: Derselbe Ereignisschlüssel darf nur eine fachliche Wirkung auslösen, auch wenn der Transport ihn erneut liefert.

Ein echter Broker schlägt den Fake nur bei echten Verbindungsfehlern

Für die nächste Teststufe stehen zwei Optionen gegeneinander: ein In-Process-Fake für den MQTT-Client und ein echter Eclipse Mosquitto 2.x in CI. Der Fake gewinnt bei vielen schnellen Schema- und Idempotenztests, weil er gezielt doppelte oder ungültige Payloads liefert und ohne Netzwerk startet. Er kostet Pflege: Sobald seine Callback-Reihenfolge vom echten Client abweicht, bestätigt er ein Verhalten, das draußen nicht existiert. Mosquitto gewinnt bei Session-, Reconnect- und Retained-Message-Tests, weil ein realer Broker die Protokollzustände verwaltet. Er kostet Startzeit, Port-Verwaltung und eine explizite Konfiguration für Zugriff und Persistenz.

Diese Unterscheidung ist in den letzten zwei Jahren praktisch wichtiger geworden, weil ein Upgrade des Clients nicht nur eine Versionsnummer ändert. Die Projektdokumentation von Eclipse Paho führt für paho-mqtt 2.x die Callback-API CallbackAPIVersion.VERSION2; das ist eine veröffentlichte API-Angabe, kein Leistungsversprechen. Ein alter Fake kann weiterhin eine frühere Callback-Signatur aufrufen und damit grüne Tests liefern, obwohl der aktualisierte Client im Betrieb anders auf eine Trennung reagiert. Pinne daher die Paho-Version im Test-Environment und führe mindestens einen Verbindungsabbruchtest gegen den tatsächlich eingesetzten Broker aus.

Ich würde nicht sämtliche Tests sofort auf echte Boards und einen permanent laufenden Broker verlagern: Dann werden einfache Parserfehler von Stromversorgung, WLAN und gemeinsam genutztem Testzustand verdeckt. Nutze ein Board gezielt dort, wo der Fake die entscheidende Eigenschaft nicht kennt – etwa beim Verhalten nach einem OTA-Rollback, beim Verlust lokaler Sequenzdaten oder bei einem Reboot zwischen Empfang und Bestätigung. Den Broker brauchst du früher als das Board, sobald dein Risiko an MQTT-Sessions und Zustellwiederholungen hängt.

Den breiten Blick auf vernetzte Systeme – IoT und vernetzte Systeme in der Softwareentwicklung – würde ich für diese Übernahme bewusst verengen: Teste eine konkrete Verbindung zwischen Geräteereignis und Datenbankeffekt, bevor du die gesamte Architektur abbildest. Diese Begrenzung ist keine Geringschätzung der übrigen Komponenten; sie verhindert, dass ein großes Testdiagramm die ungeprüfte Zustandsgrenze verdeckt.

Testabdeckung ist hier ein schlechteres Ziel als ein nachweisbarer Fehlerpfad

Der alte Reflex, zuerst eine Coverage-Schwelle festzulegen, ist besonders bei übernommenem IoT-Code riskant: Eine Zeile für „Nachricht empfangen“ kann ausgeführt sein, ohne dass jemals ein verlorener ACK, ein Neustart oder eine alte Firmware-Version beteiligt war. Für den ersten Sprint würde ich keine Prozentzahl zur Abnahmebedingung machen. Verlange stattdessen einen reproduzierbaren Fehlerpfad: Ereignis empfangen, Wirkung speichern, Verbindung vor Bestätigung trennen, Ereignis erneut zustellen und die Wirkung zählen.

Miss dabei zwei Dinge getrennt. Die Duplikatquote beschreibt, wie häufig Ereignisse mit bereits bekanntem Schlüssel ankommen; sie ist ein Signal für Transport und Geräteverhalten, kein automatischer Anwendungsfehler. Die Doppelwirkungsquote zählt dagegen, wie häufig derselbe Schlüssel mehr als eine fachliche Wirkung erzeugt. Als vorläufige Abnahmegrenze setze ich für den gezielt erzeugten Wiederholungsfall 0 Doppelwirkungen; dieser Wert ist ein zu prüfendes Testziel, keine Behauptung über die bestehende Produktion. Notiere zusätzlich, ob dein System Duplikate verwirft, protokolliert oder als auffällig meldet – andernfalls verschwindet ein wachsendes Verbindungsproblem hinter korrekter Idempotenz.

Ergänze dann Fehlerfälle, die dieselbe Grenze aus anderen Richtungen belasten. Ein Payload mit unbekannter Schemaversion darf nicht stillschweigend als aktueller Befehl gelten, weil fehlinterpretierte Felder schwerer zu entdecken sind als eine explizite Ablehnung. Ein abgelaufenes Zertifikat in einer TLS-Verbindung muss als Verbindungsfehler sichtbar werden, weil ein stiller Fallback auf unverschlüsselten Transport die Testaussage entwertet. Ein HTTP-Status 429 an einer nachgelagerten API braucht einen begrenzten Retry, weil unbegrenzte Wiederholungen aus einer kurzen Drosselung eine dauerhafte Warteschlange machen können. Für solche Fälle ist OpenTelemetry nützlich, wenn Trace- oder Log-Einträge denselben Ereignisschlüssel tragen; ohne diese Verknüpfung zeigen Traces nur, dass etwas langsam war, nicht welches Ereignis doppelt wirkte.

Auch ein Security-Scanner ist kein Ersatz für diese Prüfung. Er kann eine bekannte verwundbare Bibliotheksversion melden, aber nicht entscheiden, ob ein Update den gespeicherten Geräte-Status korrekt migriert. Prüfe deshalb Updates mit demselben Ereignisvertrag wie den Normalbetrieb: Was geschieht mit einem bereits empfangenen, noch nicht bestätigten Ereignis nach Deployment und Rollback? Genau an dieser Stelle treffen die neueren Anforderungen an nachvollziehbare Versionen auf einen alten, weiterhin aktuellen QA-Grundsatz: Nur beobachtbares Verhalten lässt sich verlässlich freigeben.

Beginne morgen mit einem einzigen wiederholten Ereignis

Nimm eine echte, anonymisierte Nachricht aus dem Bestand und finde den Codepfad bis zu ihrer dauerhaften Wirkung. Schreibe einen Test, der denselben Ereignisschlüssel zweimal einspeist und die Wirkung danach zählt. Dokumentiere Firmware-Version, Backend-Commit und Client-Version daneben. Wenn der Test rot wird, hast du einen konkreten Defekt; wenn er grün wird, trenne als Nächstes die Verbindung vor der Bestätigung. So wächst die Testsuite aus einem überprüfbaren Risiko statt aus einer Coverage-Vorgabe.