IEC 61131-3: Unit-Tests

Unit-Tests sind ein unentbehrliches Hilfsmittel für jeden Programmierer, um die Funktionsfähigkeit seiner Software sicherzustellen. Programmfehler kosten Zeit und Geld, daher benötigt man eine automatisierte Lösung, um diesen Fehlern auf die Spur zu kommen – und zwar möglichst bevor die Software zum Einsatz kommt. Unit-Tests sollten überall dort eingesetzt werden, wo professionell Software entwickelt wird. Dieser Artikel soll einen schnellen Einstieg und ein Verständnis für den Nutzen der Unit-Tests ermöglichen.

Motivation

Häufig werden für das Testen von Funktionsblöcken separate Testprogramme geschrieben. In solch einem Testprogramm wird eine Instanz des gewünschten Funktionsblock angelegt und aufgerufen. Dabei werden die Ausgangsvariablen beobachtet und manuell auf Korrektheit geprüft. Stimmen diese nicht mit den erwarteten Werte überein, so wird der Funktionsblock solange angepasst, bis der Funktionsblock das gewünschte Verhalten aufweist.

Doch mit dem einmaligen Testen von Software ist es nicht getan. So führen Änderungen oder Erweiterungen an einem Programm immer wieder dazu, dass Funktionen oder Funktionsblöcke, die zuvor ausgetestet wurden und fehlerfrei funktionierten, plötzlich nicht mehr korrekt arbeiten. Auch kommt es vor, dass sich die Behebung von Programmfehlern auch auf andere Programmteile auswirkt und somit an anderen Stellen im Code zu Fehlfunktionen führen kann. Die zuvor ausgeführten und abgeschlossenen Tests müssen somit manuell wiederholt werden.

Ein mögliche Herangehensweise für eine Verbesserung dieser Arbeitsweise besteht darin, die Tests zu automatisieren. Dazu wird ein Test-Programm entwickelt, welches die Funktionalität des zu testenden Programms aufruft und die Rückgabewerte überprüft. Ein einmal geschriebenes Testprogramm bietet eine Reihe von Vorteilen:

– Die Tests sind automatisiert und mit gleichen Rahmenbedingung (Timings, …) somit jederzeit wiederholbar.

– Einmal geschriebene Tests bleiben auch für andere Mitglieder des Teams erhalten.

Unit-Tests

Ein Unit-Test prüft einen sehr kleinen und autarken Teil (Unit) einer Software. In der IEC 61131-3 ist dieses ein einzelner Funktionsblock oder eine Funktion. Bei jedem Test wird die zu testende Einheit (Funktionsblock, Methode oder Funktion) mit Testdaten (Parametern) aufgerufen und deren Reaktion auf diese Testdaten geprüft. Stimmt das gelieferte Ergebnis mit dem erwarteten Ergebnis überein, so gilt der Test als bestanden. Ein Test besteht im Allgemeinen aus einer ganzen Reihe von Testfällen, die nicht nur ein Soll-Ist-Paar prüft, sondern gleich mehrere.

Welche Test-Szenarien der Entwickler implementiert, bleibt ihm überlassen. Sinnvoll ist es aber mit Werten zu testen, die typischerweise auch bei deren Aufruf in der Praxis auftreten. Auch die Betrachtung von Grenzwerten (extrem große oder kleine Werte) oder besonderen Werten (Null-Zeiger, Leerstring), ist sinnvoll. Liefern all diese Testszenarien erwartungsgemäß die korrekten Werte, so kann der Entwickler davon ausgehen, dass seine Implementierung korrekt ist.

Ein positiver Nebeneffekt ist der, dass es dem Entwickler weniger Kopfschmerzen bereitet komplexe Änderungen an seinem Code vorzunehmen. Schließlich kann er nach derartigen Änderungen das System jederzeit überprüfen. Treten also nach einer solchen Änderung keine Fehler auf, so ist sie höchstwahrscheinlich geglückt.

Man darf dabei allerdings die Gefahr einer schlechten Implementierung der Tests nicht außer Acht lassen. Sind diese unzureichend oder gar falsch, liefern aber ein positives Ergebnis, so führt diese trügerische Sicherheit früher oder später zu großen Problemen.

Das Unit-Test Framework TcUnit

Unit-Test Frameworks bieten die notwendigen Funktionalitäten an, um Unit-Tests schnell und effektiv zu erstellen. Durch ergeben sich weitere Vorteile:

– Jeder aus dem Team kann die Tests schnell und einfach erweitern.

– Jeder ist in der Lage die Tests zu starten und das Ergebnis der Tests auf Korrektheit zu überprüfen.

Im Rahmen eines Projektes ist das Unit-Test Framework TcUnit entstanden. Genaugenommen handelt es sich um eine SPS-Bibliothek, welche Methoden zur Verifizierung von Variablen bereit hält (Assert-Methoden). War eine Überprüfung nicht erfolgreich, so wird eine Statusmeldung in das Ausgabefenster ausgegeben. Enthalten sind die Assert-Methoden in dem Funktionsblock FB_Assert.

Je Datentyp gibt es eine Methode, wobei der Aufbau immer ähnlich ist. Es gibt immer einen Parameter der den Istwert enthält und einen Parameter für den Sollwert. Stimmen beide überein, gibt die Methode TRUE zurück, ansonsten FALSE. Der Parameter sMessage gibt den Ausgabetext vor, der im Falle eines Fehlers ausgegeben wird. Dadurch lassen sich die Meldungen den einzelnen Testfällen zuordnen. Die Namen der Assert-Methoden beginnen immer mit AreEqual.

Hier als Beispiel die Methode um eine Variable vom Typ Integer auf Gültigkeit zu überprüfen.

Pic01

Manche Methode enthalten noch zusätzliche Parameter.

Pic02

Für alle Standarddatentypen (BOOL, BYTE, INT, WORD, STRING, TIME, …) sind entsprechende Assert-Methoden vorhanden. Aber auch einige spezielle Datentypen, wie z.B. AreEqualMEM zur Prüfung eines Speicherbereichs oder AreEqualGIUD, werden unterstützt.

Ein erstes Beispiel

Unit-Tests werden dazu verwendet einzelne Funktionsblöcke unabhängig von anderen Komponenten zu überprüfen. Diese Funktionsblöcke können sich in einer SPS-Bibliothek oder in einem SPS-Projekt befinden.

Für das erste Beispiel soll sich der zu testende FB in einem SPS-Projekt befinden. Es handelt sich hierbei um den Funktionsblock FB_Foo.

Pic03

Definiert die Zeit, die der Ausgang bOut gesetzt bleibt, falls keine weiteren positiven Flanken an bSwitch angelegt werden.

bSwitchDurch eine positive Flanke wird der Ausgang bOut auf TRUE gesetzt. Dieser bleibt für die Zeit tDuration aktiv. Ist der Ausgang schon gesetzt, so wird die Zeit tDuration neu gestartet.
bOffDer Ausgang bOut wird durch eine positive Flanke unmittelbar zurückgesetzt.
tDurationDefiniert die Zeit, die der Ausgang bOut gesetzt bleibt, falls keine weiteren positiven Flanken an bSwitch angelegt werden.

Durch Unit-Tests soll bewiesen werden, dass sich der Funktionsblock FB_Foo wie erwartet verhält. Den Code zum Testen wird hierbei direkt in dem TwinCAT Projekt implementieren.

Projektaufbau

Um den Test-Code von der Applikation zu trennen, wird der Ordner TcUnit_Tests angelegt. In diesem Ordner wird der POU P_Unit_Tests abgelegt von dem aus die jeweiligen Testfälle aufgerufen werden.

Für jeden FB wird ein entsprechender Test-FB angelegt. Dieser hat den gleichen Namen plus den Postfix _Tests. Für unser Beispiel ergibt sich der Name FB_Foo_Tests.

Pic04

In P_Unit_Tests wird eine Instanz von FB_Foo_Tests angelegt und aufgerufen.

PROGRAM P_Unit_Tests
VAR
  fbFoo_Tests : FB_Foo_Tests;
END_VAR

fbFoo_Tests();

In FB_Foo_Tests befindet sich der gesamte Test-Code zur Überprüfung von FB_Foo. Hierzu werden in FB_Foo_Tests jeweils pro Testfall eine Instanz von FB_Foo angelegt. Diese werden mit unterschiedlichen Parametern aufgerufen und die Rückgabewerte werden mit Hilfe der Assert-Methoden validiert.

Die Abarbeitung der einzelnen Testfälle geschieht in einer Statemachine, die auch von der SPS-Bibliothek TcUnit verwaltet wird. Dadurch wird z.B. der Test automatisch beendet, sobald ein Fehler erkannt wurde.

Definition der Testfälle

Zuvor müssen die einzelnen Testfälle definiert werden. Jeder Testfall belegt in der Statemachine einen bestimmten Bereich.

Für die Benennung der einzelnen Testfälle haben sich einige Benennungsregeln bewährt, die helfen, den Test-Aufbau übersichtlicher zu gestalten.

Bei den Testfällen, die einen Eingang von FB_Foo prüfen sollen, setzt sich der Name zusammen aus: [Name des Eingangs]_[Testbedingung]_[erwartetes Verhalten]. Analog dazu werden Testfälle benannt, die Methoden von FB_Foo testen, also [Name der Methode]_[Testbedingung]_[erwartetes Verhalten].

Nach diesem Schema werden folgende Testfälle festgelegt:

Switch_RisingEdgeAndDuration1s_OutIsTrueFor1s

Testet, ob durch eine positive Flanke an bSwitch der Ausgang bOut für 1 s gesetzt wird, wenn tDuration auf t#1s gesetzt wurde.

Switch_RisingEdgeAndDuration1s_OutIsFalseAfter1100ms

Testet, ob durch eine positive Flanke an bSwitch der Ausgang bOut nach 1100 ms wieder FALSE wird, wenn tDuration auf t#1s gesetzt wurde.

Switch_RetriggerSwitch_OutKeepsTrue

Testet, ob durch eine erneute positive Flanke an bSwitch die Zeit tDuration neu gestartet wird.

Off_RisingEdgeAndOutIsTrue_OutIsFalse

Testet, ob durch eine positive Flanke an bOff der gesetzt Ausgang bOut auf FALSE geht.

Implementierung der Testfälle

Jeder Testfall belegt in der Statemachine mindestens einen Schritt. In diesem Beispiel wurde als Schrittweite zwischen den einzelnen Testfällen 16#0100 gewählt. Der 1. Testfall beginnt bei 16#0100, der zweite bei 16#0200, usw. In Schritt 16#0000 werden Initialisierungen durchgeführt, während der Schritt 16#FFFF vorhanden sein muss, da dieser von der Statemachine angesprungen wird, sobald eine Assert-Methode einen Fehler festgestellt hat. Läuft der Test fehlerfrei durch, so wird in 16#FF00 eine Meldung ausgegeben und der Unit-Test für FB_Foo ist beendet.

Das Pragma region ist hierbei sehr hilfreich, um die Navigation im Quellcode zu vereinfachen.

FUNCTION_BLOCK FB_Foo_Tests
VAR_INPUT
END_VAR
VAR_OUTPUT
  bError : BOOL;
  bDone : BOOL;
END_VAR
VAR
  Assert : FB_ASSERT('FB_Foo');
  fbFoo_0100 : FB_Foo;
  fbFoo_0200 : FB_Foo;
  fbFoo_0300 : FB_Foo;
  fbFoo_0400 : FB_Foo;
END_VAR

CASE Assert.State OF
{region 'start'}
16#0000:
  bError := FALSE;
  bDone := FALSE;
  Assert.State := 16#0100;
{endregion}

{region 'Switch_RisingEdgeAndDuration1s_OutIsTrueFor1s'}
16#0100:
  fbFoo_0100(...
  ...
  Assert.State := 16#0200;
{endregion}

{region 'Switch_RisingEdgeAndDuration1s_OutIsFalseAfter1100ms'}
16#0200:
  fbFoo_0200(...
  ...
  Assert.State := 16#0300;
{endregion}

{region 'Switch_RetriggerSwitch_OutKeepsTrue'}
16#0300:
  fbFoo_0300(...
  ...
  Assert.State := 16#0400;
{endregion}

{region 'Off_RisingEdgeAndOutIsTrue_OutIsFalse'}
16#0400:
  fbFoo_0400(...
  ...
  Assert.State := 16#FF00;
{endregion}

{region 'done'}
16#FF00:
  Assert.PrintPassed('Done');
  Assert.State := 16#FF10;

16#FF10:
  bDone := TRUE;
{endregion}

{region 'error'}
16#FFFF:
  bError := TRUE;
{endregion}

ELSE
  Assert.StateMachineError();
END_CASE

Für jeden Testfall gibt es eine separate Instanz von FB_Foo. Dadurch wird sichergestellt, dass jeder Testfall mit einer neu initialisierten Instanz von FB_Foo arbeitet. Eine gegenseitig Beeinflussung der Testfälle wird dadurch vermieden.

Im einfachsten Fall, besteht ein Testfall nur aus einem Schritt:

16#0100:
  fbFoo_0100(bSwitch := TRUE, tDuration := T#1S);
  Assert.AreEqualBOOL(TRUE, fbFoo_0100.bOut, 'Switch_RisingEdgeAndDuration1s_OutIsTrueFor1s');
  tonDelay(IN := TRUE, PT := T#900MS);
  IF (tonDelay.Q) THEN
    tonDelay(IN := FALSE);
    Assert.State := 16#0200;
  END_IF

Der zu testende Baustein wird für 900 ms aufgerufen. Während dieser Zeit muss bOut TRUE sein, da bSwitch auf TRUE gesetzt wurde und tDuration 1 s beträgt. Die Assert-Methode AreEqualBOOL prüft den Ausgang bOut. Hat dieser nicht den erwarteten Zustand, so wird eine Fehlermeldung ausgegeben. Nach 900 ms wird durch Setzen der Eigenschaft State von FB_Assert in den nächsten Testfall gewechselt

Ein Testfall kann auch aus mehreren Schritten bestehen:

16#0300:
  fbFoo_0300(bSwitch := TRUE, tDuration := T#500MS);
  Assert.AreEqualBOOL(TRUE, fbFoo_0300.bOut, 'Switch_RetriggerSwitch_OutKeepsTrue');
  tonDelay(IN := TRUE, PT := T#400MS);
  IF (tonDelay.Q) THEN
    tonDelay(IN := FALSE);
    fbFoo_0300(bSwitch := FALSE);
    Assert.State := 16#0310;
  END_IF

16#0310:
  fbFoo_0300(bSwitch := TRUE, tDuration := T#500MS);
  Assert.AreEqualBOOL(TRUE, fbFoo_0300.bOut, 'Switch_RetriggerSwitch_OutKeepsTrue');
  tonDelay(IN := TRUE, PT := T#400MS);
  IF (tonDelay.Q) THEN
    tonDelay(IN := FALSE);
    Assert.State := 16#0400;
  END_IF

Das Triggern von bSwitch wird in Zeile 7 und in Zeile 12 durchgeführt. In den Zeile 3 und 13 wird geprüft ob der Ausgang gesetzt bleibt.

Ausgabe der Meldungen

Nach Ausführung aller Testfälle für FB_Foo wird eine Meldung ausgegeben (Schritt 16#FF00).

Pic05

Sollte der Fall eintreten, dass eine Assert-Methode einen Fehler erkennt, so wird dieses ebenfalls als Meldung ausgegeben.

Pic06

Ist die Eigenschaft AbortAfterFail von FB_Assert auf TRUE gesetzt, so wird bei einem Fehler der Schritt 16#FFFF angesprungen und somit der Test beendet.

Die Assert-Methoden verhindert, dass in einem Schritt die gleiche Meldung mehrfach hintereinander ausgegeben wird. Die mehrfache Ausgabe der gleichen Meldung, z.B. in einer Schleife, wird somit unterdrückt. Durch Setzen der Eigenschaft MultipleLog auf TRUE wird dieser Filter deaktiviert und jede Meldung kommt zur Ausgabe.

Durch den oben gezeigten Aufbau sind die Unit-Tests klar von der eigentlichen Applikation getrennt. FB_Foo bleibt vollständig unverändert.

Diese TwinCAT-Solution wird gemeinsam mit der TwinCAT Solution für die SPS-Bibliothek in die Quellcodeverwaltung (wie z.B. TFS oder Git) abgelegt. Somit steht allen Team-Mitgliedern eines Projektes die Tests zur Verfügung. Durch das Unit-Test Framework können Tests auch von jedem erweitert und vorhandene Tests gestartet und einfach ausgewertet werden.

Auch wenn der Begriff Unit-Test Framework für die SPS-Bibliothek TcUnit etwas hochgegriffen ist, so zeigt sich doch das mit wenigen Hilfsmitteln automatisierte Tests auch mit der IEC 61131-3 möglich sind. Kommerzielle Unit-Test Framework gehen deutlich über das hinaus, was eine SPS-Bibliothek leisten kann. So enthalten diese entsprechende Dialoge um die Tests zu starten und das Ergebnis anzuzeigen. Auch werden häufig die Bereiche im Quellcode markiert, die von den einzelnen Testfällen durchlaufen wurden.

Bibliothek TcUnit (TwinCAT 3.1.4022) auf GitHub

Beispiel (TwinCAT 3.1.4022) auf GitHub

Tips

Die größte Hürde bei Unit-Tests ist häufig der innere Schweinehund. Ist dieser erst überwunden, schreiben sich die Unit-Tests fast von alleine. Die zweite Hürde stellt sich durch die Frage nach den zu testenden Teilen der Software. Es ist wenig sinnvoll, alles testen zu wollen. Vielmehr sollte man sich auf wesentliche Bereiche der Software konzentrieren und die Funktionsblöcke, welche die Basis der Anwendung ausmachen, gut testen.

Im Grunde gilt ein Unit-Test als einigermaßen qualitativ, wenn bei der Ausführung möglichst viele Zweige durchlaufen werden. Beim Schreiben der Unit-Tests sollten die Testfälle so gewählt werden, dass möglichst alle Zweige des Funktionsblocks durchlaufen werden.

Treten dann doch noch Fehler in der Praxis auf, so kann es von Vorteil sein, wenn für diesen Fehlerfall Tests geschrieben werden. Damit wird sichergestellt, dass ein Fehler, der einmal aufgetreten ist, nicht ein weiteres Mal auftritt.

Allein das zwei oder mehrere Funktionsblöcke korrekt arbeiten und dieses durch Unit-Tests bewiesen wird, bedeutet noch nicht, dass eine Anwendung diese Funktionsblöcke auch korrekt anwendet. Unit-Tests ersetzen somit in keinster Weise Integrations- und Akzeptanztests. Derartige Testmethoden validieren das Gesamtsystem und bewerten somit das große Ganze. Es ist auch unter Verwendung von Unit-Tests nötig, weiterhin das Gesamtwerk zu testen. Allerdings wird ein nicht unwesentlicher Teil potentieller Fehler schon im Vorfeld durch Unit-Tests ausgeschaltet, was im Endeffekt Aufwand für das Testen und somit Zeit und Geld spart.

Weitere Informationen

Während der Vorbereitung zu diesem Post, hat Jakob Sagatowski in seinem Blog AllTwinCAT den ersten Teil einer Artikel-Serie über Test driven development in TwinCAT veröffentlicht. Für alle die tiefer in das Thema einsteigen wollen, kann ich den Blog sehr empfehlen. Es ist erfreulich, dass auch andere SPS-Programmierer sich mit dem Testen ihrer Software auseinander setzen. Auch das Buch (Amazon-Werbelink *) The Art of Unit Testing (deutsche Ausgabe) von Roy Osherove ist ein guter Einstieg in das Thema. Auch wenn das Buch nicht für die IEC 61131-3 geschrieben wurde, so enthält es doch einige interessante Ansätze, die sich ohne Probleme auch in der SPS umsetzen lassen.

Abschließend will ich mich noch bei meinen Kollegen Birger Evenburg und Nils Johannsen bedanken. Als Grundlage für diesen Post diente eine SPS-Bibliothek, die mir freundlicherweise von beiden zur Verfügung gestellt wurde.

Author: Stefan Henneken

I’m Stefan Henneken, a software developer based in Germany. This blog is just a collection of various articles I want to share, mostly related to Software Development.

3 thoughts on “IEC 61131-3: Unit-Tests”

  1. I downloaded your TCUnit library and tried your example on this page. Is ist possible, that the api of the library changed ? Your example on this page is not working with the current version of the TcUnit library.

  2. Pingback: Website

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: