Im Jahr 2021 war bei AutoLab noch alles im grünen Bereich. Das Team sprühte vor Energie und startete voller Enthusiasmus in ein neues Kapitel. Mit dem ersten großen TwinCAT-Projekt wurde der Entschluss gefasst, eine eigene AutoLab-Library zu entwickeln, die als Basis für zukünftige Projekte dienen sollte. Parallel dazu starteten wir unser bis dato ehrgeizigstes Vorhaben: ein Highspeed-End-of-Line-Prüfautomat, der uns an die Spitze der Prüftechnik katapultieren sollte.
Monatelang sprinteten wir diesem Ziel entgegen. Dabei entstanden viele neue Funktionen, Klassen und Module, welche manuell in der Simulation getestet wurden. In den Besprechungen wurde immer durchgegangen was schon fertig ist und was bereits getestet wurde. Sätze wie “Die Ablauflogik des XTS wurde getestet und funktioniert.”, oder “Das Event-Management wurde wochenlang entwickelt und steuert die Fehlerbehandlung und Reaktion der gesamten Anlage” fielen und festigten das Vertrauen in unsere Entwicklungen. Leider war es ein trügerisches Vertrauen.
Im September sollte die Inbetriebnahme starten. Im Juli war zwar schon vieles fertig, aber gleichzeitig stand auch noch einiges an. Und genau da begannen sich merkwürdige Verhaltensweisen zu häufen. Nichts eindeutig Reproduzierbares, aber genug, um mein Bauchgefühl zu alarmieren. Ich analysierte ein paar dieser Fälle, zwei volle Tage lang – bis mir plötzlich klar wurde, was los war: Der Code funktionierte nur oberflächlich. Im Detail war er durchzogen von Bugs, inkonsistenten Zuständen und stillen Fehlern. Das manuelle Testen hatte sich wohl meist auf den Standardfall fokussiert. Aber sobald einer der vielen Edge-Cases auftrat, brach vieles in sich zusammen. Es konnte auch leicht möglich sein, dass manches davon mal funktioniert hatte, es aber dann durch Änderungen in der Codebase kaputt ging – nur konnte es niemand bei uns zuverlässig merken, weil uns automatisierte Test-Cases fehlten, die so etwas systematisch zu Tage fördern würden. Was genau schieflief, war nicht mehr zu klären. Ohne Tests blieb alles Spekulation. Es war auch egal. Was zählte war, dass nur noch zwei Monate hatten. Zwei Monate, um all das auszubügeln – mit einem System, das niemand je wirklich, und mit wirklich meine ich systematisch, durchgetestet hatte.
Meine späte Realisierung der Situation konnte nur noch dadurch ausgeglichen werden, dass ich mich mit allem, was ich hatte, in die Fertigstellung des Projekts warf. Ab dann waren Wochen mit 70 bis 80 Stunden Standard für mich. Bis zur Inbetriebnahme wurden viele der größeren Bugs ausgebessert und die meisten Features vorbereitet. Leider auch noch nicht mittels sauberen Unit-Tests, sondern mit purer Willenskraft. Anfang September hatten wir dadurch einen Projektstand mit dem wir die Inbetriebnahme starten konnten. Die erste Katastrophe war somit abgewandt, das nächste Desaster jedoch schon in Sichtweite. Wir hatten nur noch 2 Monate bis zum Run@Rate, bei welchem die Maschine bei Vollauslastung ihre Leistungsfähigkeit unter Beweis stellen musste. Die großen Fehler? Die hatten wir bereits besiegt. Aber die wirklich gefährlichen waren nicht die offensichtlichen. Es waren die dutzenden kleinen. Ein kleiner Fehler konnte aber auch einen großen Crash erzeugen und damit jeglichen Zeitplan zu Nichte machen. Und genau diese kleinen Fehler traten jetzt bei der Inbetriebnahme zutage. Einer nach dem anderen. Nun sprinteten wir im überlappenden Zweischicht Betrieb zum Projektabschluss. Einer unser Programmierer in der “normalen Tagschicht”. Ich immer von 12Uhr mittags, bis 3 in der Nacht. Unterm Tag half ich bei den Robotern. Am Abend und in der Nacht brachte ich das SPS Programm auf Vordermann oder programmierte die ersten HALCON Routinen für die Bildverarbeitung. Samstag und Sonntag ging es natürlich weiter. Das fehlende Testing holte uns jetzt ein und die Deadline rückte immer näher. 2 Wochen vorm Run@Rate lief noch fast nichts, aber aufgeben war keine Option. Zwei Werkstage vorm Run@Rate lief dann das erste mal die Maschine stabil und robust. Ich konnte es fasst nicht glauben. Mit purer Willenskraft hatten wir es noch irgendwie geschafft. Das anschließende Run@Rate wurde auch mit Bravour gemeistert und die Feier im Anschluss war ausgiebig.
Das Projekt konnte zwar erfolgreich abgeschlosssen werden. Die Library stand jedoch vor einem Scherbenhaufen und war quasi unbrauchbar. Die geplanten Projekt-Stunden wurden bei weitem übertroffen. In diesem Moment wurde mir klar, dass es so nicht weitergehen konnte. Es brauchte eine grundlegende Veränderung: Libraries, die nicht nur manuell getestet waren, sondern wirklich verlässlich und automatisiert überprüft wurden – quasi mit einem Zero-Trust-Ansatz. Und so startete die erste Library, die konsequent mit Unit Testing entwickelt wurde. Schon bald zeigte sich, dass dieser neue Ansatz Früchte trug: Die Funktionen der Library waren zuverlässig und die Fehlerquote sank drastisch. Dies wurde auch eindrucksvoll bei der nächsten Inbetriebnahme bestätigt. Diese lief dank der getesteten Libraries fast reibungslos und war, dieses Mal ohne Überstunden, in wenigen Wochen abgeschlossen. Versteckte Bugs gehörten der Vergangenheit an.
Heute bin ich überzeugt: Unit-Testen hat die AutoLab gerettet. Hätten wir es nicht eingeführt, hätten wir vielleicht noch ein oder zweimal Glück gehabt, aber unser Untergang wäre unausweichlich gewesen. Ohne Tests verlässt man sich auf sein Glück. Mit Tests verlässt man sich auf saubere, systematische Validierung.
Was sind Unit Tests und warum sind sie wichtig?
Unit-Tests
Unit Tests sind automatisierte Tests, die einzelne, isolierte Einheiten deines Codes überprüfen – typischerweise Funktionen oder Methoden. Sie helfen dir sicherzustellen, dass diese kleinen Codebausteine genau das tun, was sie sollen. Dabei wird jeder Test so geschrieben, dass er unabhängig vom Rest der Anwendung läuft.
Stell dir vor, du entwickelst eine App, die Rabatte auf Produkte berechnet. Eine Funktion darin soll 10 % Rabatt auf einen Preis anwenden.
Ein Unit Test würde nun genau diese eine Funktion testen – unabhängig davon, ob die App eine Datenbank hat, ob sie im Browser läuft oder ob noch andere Features existieren.
Beispiel:
Du gibst 100 € in die Funktion – der Test prüft, ob 90 € herauskommen.
Dann testest du 50 € → Erwartung: 45 €.
Auch Sonderfälle kannst du prüfen: z. B. 0 € oder negative Werte
Das Ziel: Du willst sichergehen, dass diese eine Rabattberechnung korrekt funktioniert – ganz für sich allein.
In deiner echten Anwendung oder Library sind die Funktionen, die du testest, oft deutlich komplexer. Zum Beispiel das Aufsynchronisieren von Bewegungssteuerungen. Das Gute ist jedoch: Den Soll-Wert zu definieren ist meist erstaunlich einfach. Stimmt der Ist-Wert später damit überein, wird der Unit Test bestanden. Wenn nicht, schlägt er fehl und du weißt sofort, wo du ansetzen musst. Und das wichtigste: einmal geschrieben, kannst der Test automatisch und systematisch bei jeder Änderung in der Codebase ausgeführt werden!
Hierbei spielt es keine Rolle, in welcher Programmiersprache oder Umgebung du arbeitest. Ob Python, TwinCAT oder MATLAB – das Prinzip bleibt dasselbe: eine Funktion mit bestimmten Inputs aufrufen und das Ergebnis gegen den erwarteten Sollwert prüfen.
In Python arbeite ich etwa mit pytest, in TwinCAT mit TcUnit, und in MATLAB verwende ich scriptbasierte Tests, die sich direkt über runtests starten lassen.
Integrationstests: Der nächste Schritt
Während Unit Tests einzelne Bausteine prüfen, testen Integrationstests, ob mehrere Komponenten zusammen funktionieren. Zum Beispiel: Ein Bediener stellt per Touchpanel einen Sollwert ein. Der Test prüft, ob dieser Wert korrekt in der SPS ankommt und dort verarbeitet wird, inklusive Rückmeldung an die Oberfläche. Solche Tests sind meist komplexer, laufen langsamer, decken aber genau die kritischen Übergänge und Schnittstellen auf, an denen Systeme häufig versagen.
In der Praxis verschwimmt die Grenze zwischen Unit- und Integrationstest oft.
Man kann sich natürlich auf eine wissenschaftliche Definitionsdebatte einlassen, was meiner Meinung nach aber reine Zeitverschwendung ist. Entscheidend ist, dass sauber getestet wird. Dazu gehören sowohl Unit- als auch Integrationstests.
Das Gute daran: Vom Tooling her ist es egal. Ob pytest oder TcUnit – beides eignet sich für beide Testarten. Der Unterschied liegt nur im Ziel: Entweder prüfst du das Ergebnis einer einzelnen Funktionseinheit, oder das Ergebnis einer ganzen Kette von Funktionen, die integriert zusammenarbeiten.
Test Driven Development (TDD): Erst testen, dann coden
TDD ist ein Entwicklungsansatz, bei dem du den Test vor dem eigentlichen Code schreibst. Du überlegst dir zuerst, was eine Funktion leisten soll und formulierst genau das als automatisierten Test.
Anschließend schreibst du nur so viel Code, dass der Test erfolgreich durchläuft. Wenn das geschafft ist, bringst du Struktur in den Code: aufräumen, kürzen, umbenennen, wiederverwenden. Wichtig dabei ist, dass sich am Verhalten nichts ändert – der Test muss weiterhin bestehen.
TDD hilft dabei, den Code besser zu durchdenken, modularer zu schreiben und Fehler frühzeitig abzufangen. Gerade bei wachsender Komplexität zahlt sich das langfristig aus.
Vor allem der Punkt mit der Modularität ist meiner Meinung nach ein großartiges Nebenprodukt. TDD hat nicht per se das Ziel, den Code modularer zu machen, aber es zwingt einen ganz automatisch dazu, Funktionen in kleine, überschaubare Einheiten zu zerlegen, die möglichst unabhängig von anderen Methoden sind. Meine Erfahrung: Je mehr TDD ich mache, desto sauberer wird mein Code – scheinbar wie von Zauberhand.
Wer schreibt eigentlich die Tests?
Bei TDD ist es üblich, dass dieselbe Person sowohl den Test als auch den zugehörigen Code schreibt. Erst formuliert man einen Test für ein bestimmtes Verhalten, dann schreibt man die Funktion, die dieses Verhalten erfüllt. Danach folgt der nächste Test, die nächste Funktion – Schritt für Schritt. Meiner Erfahrung nach hat das auch einen ganz praktischen, mentalen Vorteil. Statt auf einen großen Reward am Ende hinzuarbeiten, erlebt man über den Tag hinweg viele kleine Zwischenerfolge. Ich persönlich habe so viel mehr Spaß am programmieren.
Muss man zuerst alle Tests schreiben?
Nein. Bei TDD geht es nicht darum, im Voraus den kompletten Testkatalog einer Klasse runterzuschreiben. Ganz im Gegenteil. Man beginnt mit einem kleinen, konkreten Test, der ein bestimmtes Verhalten beschreibt. Dann schreibt man den dazugehörigen Code. Wenn das passt, kommt der nächste Test, die nächste Funktion.
So entsteht der sauberer Code schrittweise, nachvollziehbar und jederzeit überprüfbar.
Herausforderungen ohne Unit Tests
Ein zentrales Problem beim Verzicht auf Unit Tests liegt in der unterschiedlichen Herangehensweise der Entwickler an das manuelle Testen. Während manche gründlich vorgehen, testen andere kaum einzelne Methoden und prüfen den Code erst im Gesamtverbund, was das Debugging extrem erschwert. Hinzu kommt, dass manuelles Testen auf lange Sicht deutlich zeitaufwendiger ist. Ein Beispiel: Angenommen, das manuelle Testen einer Methode dauert etwa fünf Minuten. Das Schreiben eines automatisierten Tests mag vielleicht zwanzig Minuten dauern, aber sobald Änderungen oder Refactorings ins Spiel kommen, rechnet sich der Aufwand schnell – denn den Test kann man beliebig oft automatisiert ausführen.
Kleine Beispielrechnung:
- Manueller Test: 5 Minuten
- Automatisierter Test (einmalig schreiben): 20 Minuten
- Break-even: 20 Minuten ÷ 5 Minuten = nach 4 Durchläufen
Heißt: Schon nach viermaligem manuellen Testen hat sich der Automatisierungsaufwand amortisiert. In großen Projekten oder über Jahre genutzten Libraries wird manuelles Testen so sehr schnell zum Flaschenhals. Außerdem decken automatisierte Tests auch Fehler auf, die man sonst leicht übersieht. Einmal bin ich zum Beispiel durch Unit Tests darauf aufmerksam geworden, dass eine Änderung in unserer eigenen Library einen Bug in einer externen, eingebundenen Library ausgelöst hat, der sonst unentdeckt geblieben wäre. Solche Beispiele zeigen, wie wertvoll automatisierte Tests für die Qualitätssicherung sind.
Ein weiterer entscheidender Nachteil ist die fehlende Nachvollziehbarkeit. Automatisierte Tests dokumentieren den Testprozess und die Ergebnisse. Außerdem entdeckt man ohne automatisierte Tests viele versteckte Abhängigkeiten und Fehler nicht, die sich in komplexen Projekten einschleichen. All diese Punkte machen deutlich, warum Unit Tests so wertvoll sind und langfristig nicht nur die Qualität, sondern auch die Effizienz der Entwicklung erheblich steigern.
Best Practices für Unit Testing
Wie sehr ich Unit Tests schätze, merkt man wohl schon daran, dass dieser Blogpost inzwischen eher ein Buchkapitel geworden ist. Damit es nicht zu ausschweifend wird, habe ich die Details zu den Best Practices in einen separaten Artikel ausgelagert. Hier trotzdem die wichtigsten Merksätze daraus:
💡Jeder Bug, sein Unit-Test
Jeder Bug bekommt seinen eigenen Unit-Test. Wenn du einen Bug entdeckst, schreibe zuerst einen Unit-Tests welcher fehlschlägt. Danach erst korrigiere den Bug und behalte den Unit Tests für immer, um auch bei Refactoring sicher zu sein.
💡Teste Verhalten, nicht Implementierung
Wenn du den Code umbauen musst, nur damit der Test ihn „versteht“, testest du nicht das Verhalten – du missbrauchst die Implementierung.
💡Halte Tests klein, unabhängig und eindeutig
Ein guter Unit Test ist kompakt, steht auf eigenen Beinen und verrät dir sofort, ob alles passt.
💡Verwende aussagekräftige Testnamen
Ein guter Testname ist wie eine gute Commit-Message: Man versteht sofort, worum es geht – ganz ohne weiteren Kommentar.
💡Besser anfangen als warten
Lieber heute mit automatischen Unit-Tests starten und die Pipeline manuell triggern, als monatelang auf die perfekte Automatisierung zu warten.
Erfolgsgeschichten und Ergebnisse
Ich bin wirklich davon überzeugt, dass TDD unsere Firma gerettet hat. Die Gründe dafür sind mannigfaltig. Bei den Anlagen, die wir noch ohne Unit Testing entwickelt haben, hatten wir auch nach der Inbetriebnahme regelmäßig kleinere Bugs. Nichts Ernsthaftes, das die Produktion gefährdet hätte, aber lästig genug, um vor allem unerfahrenen Bedienern das Leben schwer zu machen und uns viel Zeit zu kosten.
Bei allen Projekten mit TDD hat sich die Inbetriebnahmezeit drastisch verkürzt – bei ähnlicher Größe von etwa 30 auf nur 8 Wochen. Und nach der Inbetriebnahme ist das Thema meist durch. Ab und zu kommt vielleicht noch eine Rückmeldung mit Wünschen zur besseren Bedienbarkeit, aber die Funktionalitäten laufen einfach.
Ein extremer Vorteil ist auch das Vertrauen in unsere Libraries. Wenn man so komplexe Maschinen entwickelt wie wir, hat man immer dutzende Aktoren und Sensoren, die miteinander zusammenspielen, dazu verschiedene Modi und Bedienereingaben. Früher, wenn seltsame Effekte auftraten, stand man vor der Frage: Liegt der Bug in Komponente A, B, C, D, E oder F?
Heute wissen wir meist sehr schnell, wo wir suchen müssen, weil über 90 % des Codes in Libraries steckt, die nahezu zur Perfektion getestet wurden. Zu 99,99 % liegt der Fehler nicht dort, und man kann eine erhebliche Anzahl von Fehlerquellen innerhalb weniger Sekunden ausschließen.
Erst letztens hatten wir den Fall, dass eine Anlage immer wieder mal durch einen Fehler stoppte, der meldete: „Multiple, widersprechende Endlagen bei Zylinder XY.“
Dank unserer Funktion, dass wir nach jedem Fehlerfall automatisch alle History-Listen mit allen Schrittwechseln ablegen, konnten wir das auch nachvollziehen – selbst wenn es in der Nachtschicht passiert.
Die Instandhaltung schaute sich die Lage an, bewegte den Zylinder und versicherte, dass die Sensoren richtig eingestellt sind und der Fehler nicht plausibel ist. Der erste Impuls von vielen: Die Library hat einen Bug.
Da wir aber zu genau diesem Fall zahlreiche Unit- und Integrationstests hatten, konnte ich das mit an Sicherheit grenzender Wahrscheinlichkeit ausschließen. Dadurch blieben nur noch wenige Möglichkeiten. Eine davon: Die Filterzeit für die Endlagen war zu hoch eingestellt. Durch die Signalverzögerung kam es manchmal zum Fehler.
Der Grund war also innerhalb von Minuten gefunden – anstatt dass ein Entwickler stundenlang im Code auf Fehlersuche gehen muss. Die Library wurde anschließend so aktualisiert, dass man als Bediener nur noch Filterzeiten einstellen kann, die Sinn machen und solche Pseudofehler unmöglich werden.
Sogar gerade eben, während ich diesen Blogpost schreibe, hat ein Entwickler von uns von einem komischen Edge Case berichtet: Der Event Logger von Twicat bekommt ein Problem, wenn man bei einem Alarm neu konfiguriert, ob man eine Confirmation vom Operator braucht oder nicht. Dank der zahlreichen Tests mussten wir nicht die Nadel im Heuhaufen suchen, sondern wussten innerhalb von Minuten, dass es nur ein Edge Case sein kann, der auftritt, wenn man diese Änderung im FB_init macht. Früher hätten wir für die Analyse vielleicht einen ganzen Tag gebraucht – jetzt geht das in wenigen Minuten.
Würde ich wollen, könnte ich noch hunderte Vorteile auflisten, schließe aber mit einem nicht unwesentlichen ab: dem Einlernen von Junior-Entwicklern. Ich kann mich noch gut daran erinnern, als wir im Rahmen einer Bachelorarbeit an einem generischen Achs-Baustein arbeiteten. Für das Enablen und wieder Disablen der Achse benötigte die Entwicklerin 15 Minuten. Als wir dann gemeinsam mit ihr Unit Tests einführten und ihr TDD näherbrachten, kam ein Mini-Bug nach dem anderen zum Vorschein. Das Ganze dauerte am Ende 3 Stunden.
Der ein oder andere wird hier vielleicht aufschreien und sagen: „Ja genau, da sieht man, wie viel Zeit man mit TDD verschwendet.“
Ich sehe es aber als absoluten Erfolg. Dank des Testens legt man den Fokus auf eine astreine Implementierung. Der Entwickler lernt, wie viele Kleinigkeiten selbst bei einfachen Funktionen zusammenspielen. Und ich als Teamlead weiß, dass wir uns bei der Inbetriebnahme nicht nur Stunden, sondern oft Tage oder sogar Wochen sparen – weil wir dem Fundament dank der Tests zurecht vertrauen können.
Fazit und Ausblick
Am Ende lässt sich festhalten, dass es für uns bei AutoLab ein absoluter Gamechanger war, TDD einzuführen. Es hat unsere Libraries modularer, besser wartbar und um ein Vielfaches weniger fehleranfällig gemacht. Verglichen mit den Problemen, die wir vorher ohne TDD hatten, ist es meiner Meinung nach keine Übertreibung zu sagen, dass es uns „gerettet“ hat.
Nichtsdestotrotz – vor allem im SPS-Bereich stehen die Frameworks noch ziemlich am Anfang, und hier kann wahrscheinlich die gesamte Branche noch erhebliche Potenziale heben. Wir werden auf jeden Fall versuchen, das weiter voranzubringen, indem wir kontinuierlich die Vorteile aufzeigen und es auch im Rahmen unserer Beratungsleistungen aktiv anpreisen.
Wenn die industrielle Automatisierungstechnik beim Thema TDD noch hinter dem Stand der allgemeinen Softwareentwicklung hinterherhinkt, dann ist es im Forschungsbereich wahrscheinlich noch viel schlimmer. Das durfte und darf ich im Rahmen meiner wissenschaftlichen Tätigkeiten beobachten – und ich versuche hier auch gezielt gegenzusteuern. Ich bin zutiefst davon überzeugt, dass TDD auch zu besseren Forschungsarbeiten führt, da mittlerweile fast jeder technische oder naturwissenschaftliche Bereich zu großen Teilen auf Software angewiesen ist.
Hierzu kann ich gerne meine beiden vergangenen Blogposts Wissenschaftliche Arbeiten betreuen – Chancen, Aufwand und Nutzen und Was Wirtschaft und Wissenschaft voneinander lernen können empfehlen, in denen ich diese Tatsache näher beleuchte.
Schreibe einen Kommentar