XML testen / Geistreich, aber falsch gerechnet?

Geistreich, aber falsch gerechnet?

Geistreich, aber falsch gerechnet?

➪ Systematisches Testen stellt sicher, dass ein Programm die gestellten Anforderungen erfüllt.

Bevor Sie eine eMail versenden, schauen Sie noch mal drüber, ob auch alles so stimmt, was Sie geschrieben haben? Falls ja: Testen Sie auch Ihre Programme, die Sie schreiben, mit derselben Sorgfalt? Diese Programme generieren täglich tausendfach, vielleicht millionenfach einen Output, der viel Geld kosten oder auch Menschenleben gefährden kann. Was hilft es, wenn Sie zwar geistreich, aber falsch rechnen? Woran machen Sie es fest, ob Ihr Programm korrekt arbeitet?

Wie oft habe ich mit IT-lern über systematisches und automatisiertes Testen diskutiert, wie oft habe ich das Argument gehört, dass Testen pure Zeitverschwendung sei. Testen erübrige sich, weil die Sache doch völlig klar sei, so die Begründung. "Ich habe eine klare Vorstellung, was der Rechner machen soll, und in dem Sinn programmiere ich ihn. Dann prüfe ich optisch an einigen zufällig ausgewählten Beispielen, ob meine Annahme stimmt. Damit habe ich genug getan. Wenn es um eine simple Multiplikation geht, dann teste ich: 2*3 ist 6, und 3*4 ist 12. Das reicht doch, oder nicht? Und für alle komplexeren Fälle gilt: Man kann nicht ALLES testen. Also fange ich damit erst gar nicht an."

Ist das wirklich so? Macht Ihr Rechner garantiert genau das, was Sie sich vorstellen? Nehmen Sie Ihre simple Multiplikation. Berechnen Sie 7 * 25.99. Erhalten Sie immer und unter allen Umständen den Wert 181.93, den Sie vermutlich erwarten? Oder lautet Ihr Ergebnis vielleicht 181.92999999999998?

Gegenprobe

Für den Einstieg in das automatisierte Testen müssen Sie sich nicht einmal mit einem der zahlreichen professionellen Test-Frameworks herumschlagen. Was spricht dagegen, Ihre erwarteten Ergebnisse in den Code hineinzuschreiben und den Rechner prüfen zu lassen, ob Ihre Erwartungen zutreffen? So brauchen Sie sich die Arbeit nur ein einziges Mal zu machen, und der Rechner kann den Soll-Ist-Abgleich beliebig oft und in enormer Geschwindigkeit (jedenfalls schneller als Sie mit optischer Kontrolle) durchführen.

Ihr nächster Schritt auf dem Weg zum professionellen Tester besteht also darin, eine ganze Anzahl von Testfällen zu schreiben. Im folgenden Beispiel habe ich einige Testfälle in eine Variable "vtestsequenz" geschrieben. Jedes einzelne "test"-Element besitzt drei Attribute: die beiden Parameter p1 und p2 sowie das erwartete Ergebnis.

Im Programmaufruf wird mit xsl:for-each select="$vtestsequenz/test jeder Einzeltest ausgewertet: Die Parameter p1 und p2 werden dem Template Multipliziere übergeben, das jeweilige Resultat steht in der Variablen veinzeltest. Die Variable verwartet erhält den Inhalt des aktuellen Erwartungswertes. Weichen die Werte von veinzeltest und verwartet voneinander ab, so gibt es eine Fehlermeldung.


<xsl:stylesheet version="2.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
  xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <xsl:variable name="vtestsequenz">  
    <test p1="12.99" p2="2" erwartet="25.98" />
    <test p1="12.99" p2="3" erwartet="38.97" />
    <test p1="12.99" p2="4" erwartet="51.96" />
    <test p1="12.99" p2="5" erwartet="64.95" />
    <test p1="12.99" p2="6" erwartet="77.94" />
    <test p1="12.99" p2="7" erwartet="90.93" />
    <test p1="12.99" p2="8" erwartet="103.92" />
    <test p1="12.99" p2="9" erwartet="116.91" />        
    <test p1="12.99" p2="11" erwartet="142.89" />
    <test p1="12.99" p2="22" erwartet="285.78" />
    <test p1="12.99" p2="33" erwartet="428.67" />
    <test p1="12.99" p2="44" erwartet="571.56" />
    <test p1="12.99" p2="55" erwartet="714.45" />
    <test p1="151.23" p2="6" erwartet="907.38" />
    <test p1="151.23" p2="5" erwartet="756.15" />
    <test p1="151.23" p2="4" erwartet="604.92" />
    <test p1="151.23" p2="3" erwartet="453.69" />
    <test p1="151.23" p2="2" erwartet="302.46" />    
    <test p1="25.99" p2="2" erwartet="51.98"/>
    <test p1="25.99" p2="3" erwartet="77.97"/>
    <test p1="25.99" p2="4" erwartet="103.96"/>
    <test p1="25.99" p2="5" erwartet="129.95"/>
    <test p1="25.99" p2="6" erwartet="155.94"/>
    <test p1="25.99" p2="7" erwartet="181.93"/>
    <test p1="25.99" p2="8" erwartet="207.92"/>
    <test p1="25.99" p2="9" erwartet="233.91"/>
    <test p1="25.99" p2="10" erwartet="259.9"/>    
    <test p1="2.08" p2="3" erwartet="6.24" />
    <test p1="2.09" p2="3" erwartet="6.27" />
    <test p1="2.1" p2="3" erwartet="6.3" />" 
    <test p1="2.11" p2="3" erwartet="6.33" />
    <test p1="2.12" p2="3" erwartet="6.36" />
    <test p1="2.13" p2="3" erwartet="6.39" />
    <test p1="2.14" p2="3" erwartet="6.42" />
    <test p1="2.15" p2="3" erwartet="6.45" />
    <test p1="2.16" p2="3" erwartet="6.48" />
    <test p1="2.17" p2="3" erwartet="6.51" />
    <test p1="2.18" p2="3" erwartet="6.54" />
    <test p1="2.19" p2="3" erwartet="6.57" />
    <test p1="2.2" p2="3" erwartet="6.6" />
    <test p1="2.21" p2="3" erwartet="6.63" />
    <test p1="2.22" p2="3" erwartet="6.66" />
    <test p1="2.23" p2="3" erwartet="6.69" />            
  </xsl:variable>
  <xsl:template name="Multipliziere">
    <xsl:param name="a" />
    <xsl:param name="b" />
    <xsl:value-of select="$a * $b" />
  </xsl:template>
  <xsl:template name="Multipliziere_decimalcast">
    <xsl:param name="a" as="xs:decimal" />
    <xsl:param name="b" as="xs:decimal" />
    <xsl:value-of select="$a * $b" />
  </xsl:template>
  <xsl:template match="/">
    <erg>
      <testsequenz>
        <xsl:for-each select="$vtestsequenz/test">
          <xsl:variable name="veinzeltest">
            <xsl:call-template name="Multipliziere">
              <xsl:with-param name="a">
                <xsl:value-of select="@p1" />
              </xsl:with-param>
              <xsl:with-param name="b">
                <xsl:value-of select="@p2" />
              </xsl:with-param>
            </xsl:call-template>
          </xsl:variable>
          <xsl:variable name="verwartet">
            <xsl:value-of select="@erwartet" />
          </xsl:variable>
          <xsl:choose>
            <xsl:when test="$verwartet = $veinzeltest"/>
            <xsl:otherwise>
              <FEHLER erwartet="{$verwartet}" real="{$veinzeltest}" />
              <xsl:comment>Das Ergebnis ist FALSCH:
                <xsl:value-of select="@p1" /> *
                <xsl:value-of select="@p2" /> =
                <xsl:value-of select="$veinzeltest" /> ; ERWARTET:
                <xsl:value-of select="@erwartet" />
              </xsl:comment>
            </xsl:otherwise>
          </xsl:choose>
        </xsl:for-each>
      </testsequenz>
    </erg>
  </xsl:template>
</xsl:stylesheet>

Auf diese Weise erhalte ich auf meinem System eine ganze Reihe von Fehlermeldungen. Diese bestätigen, dass Rechnersysteme intern oft anders ticken, als Developer sich das vorstellen. (Übrigens: All diese Fehlermeldungen treten nicht mehr auf, wenn Sie gezielt die Kontrolle über Ihr System übernehmen und es anweisen, jeden Parameter nicht als double, sondern als decimal zu casten, was Sie im obigen Beispiel einfach dadurch erreichen, dass Sie nicht das Template Multipliziere aufrufen, sondern Multipliziere_decimalcast.)


<erg>
  <testsequenz>
    <FEHLER erwartet="142.89" real="142.89000000000001" />
    <FEHLER erwartet="285.78" real="285.78000000000003" />
    <FEHLER erwartet="571.56" real="571.5600000000001" />
    <FEHLER erwartet="907.38" real="907.3799999999999" />
    <FEHLER erwartet="453.69" real="453.68999999999994" />
    <FEHLER erwartet="181.93" real="181.92999999999998" />
    <FEHLER erwartet="6.3" real="6.300000000000001" />
    <FEHLER erwartet="6.45" real="6.449999999999999" />
    <FEHLER erwartet="6.54" real="6.540000000000001" />
    <FEHLER erwartet="6.6" real="6.6000000000000005" />
    <FEHLER erwartet="6.69" real="6.6899999999999995" />
  </testsequenz>
</erg>

Wie Sie sehen, lief bei der obigen Zufallsauswahl in vtestsequenz etwa jeder vierte Soll-Ist-Abgleich auf einen Fehler, und das bei einer "ganz simplen" Multiplikation. Das ist schon bemerkenswert, jedenfalls Grund genug, sich ernsthaft über systematisches Testing Gedanken zu machen und dem System eben nicht blind zu vertrauen.

Für eigene Experimente empfehle ich: Variieren Sie mit einer beliebigen Sprache einen Wert, und multiplizieren Sie ihn mit einer Konstanten. Vielleicht werden Sie überrascht sein zu sehen, was Ihr System im Hintergrund tatsächlich veranstaltet, während Sie sich Wunderdinge vorstellen, was Ihr Rechner alles kann. Schon in dem Wertebereich zwischen 2 und 3 gibt's genügend Gelegenheit fürs Staunen.


Sub Main()
  For p1 = 2 To 3 Step 0.01
      Dim p2 = 3
      Dim p3 = p1 * p2
      If p3.ToString.Length > 10 Then
        Console.WriteLine("{0} * {1} = {2}", p1, p2, p3)
      End If
  Next
End Sub

Testlotterie

Spielen Sie einmal durch, wie der Software-Entwickler Harry Hilflos sein Programm testet. Harry beginnt, indem er "irgendeine" Testdatei spontan auswählt (der erste Zufall) und jene Datei gegen ein XML-Schema validiert. Dieses XML-Schema stellt eine wesentliche Grundlage für die Annahmen und Voraussetzungen der Konvertierungslogik dar.

Eventuelle Validierungs-Fehler moniert Harry beim Datenlieferanten. Fehlerhafte Testdateien verwendet Kollege Hilflos grundsätzlich nicht für weitere Programmtests: Die sind schließlich falsch. Harry sucht so lange, bis er eine Schema-valide Beispieldatei findet, die er für das Testing verwenden kann.

Ein gravierender Fehler: Jede verworfene Datei ist ein reales Beispiel, das im späteren Live-System jederzeit auftreten könnte und das daher Anlass zu gründlicher Überprüfung der Verarbeitungslogik bieten sollte. Aber gerade dieses wichtige Beispiel wird vom Test ausgenommen.

Durch den systematischen Ausschluss einer potenziell ergiebigen Fehlerquelle wird die Wahrscheinlichkeit, aussagekräftige Testdaten zu verwenden, drastisch vermindert. Die Tests werden von vornherein verfälscht. Es wird eine Datenqualität unterstellt, die es in der späteren Produktivumgebung mit hoher Wahrscheinlichkeit nicht gibt, sofern dort die Input-Validierung deaktiviert wird.

Dessen ungeachtet, schickt Harry die zweifelhafte Testdatei durch sein Programm und schaut sich das Ergebnis an: rein optisch, wohlgemerkt. Das heißt, er gleicht bei einigen spontan ausgewählten Details (der zweite Zufall) durch Augenschein ab, ob diese seinen Erwartungen entsprechen. Eine vollständige und jederzeit reproduzierbare Ergebniskontrolle des Gesamtresultats findet nicht statt. (Harry: "Das habe ich ja noch nie gemacht! Wie soll das denn funktionieren?")

Hier haben Sie den nächsten gravierenden Fehler. Die verbleibenden, ohnehin "entschärften" und daher fragwürdigen Testfälle werden nicht systematisch untersucht. Es werden keine Szenarien provoziert, die in der Programmlogik abgefangen werden sollten. Häufige Programmierfehler sowie schlechte Datenqualität werden nicht getestet.

Aber auch das stört unseren Programmierprofi nicht. Bisher hat alles so weit gepasst, also schiebt Harry seine Codezeilen sorglos in die Produktiv-Umgebung, die anders konfiguriert ist als die Testumgebung (anderes Betriebbsystem, andere Java-Version, andere Prozessoren).

Und schon wieder ein Versäumnis: Mögliche Differenzen zwischen der Test- und der Produktivumgebung überprüft unser Developer nicht systematisch, überlässt dies also abermals dem (mittlerweile dritten) Zufall. (Harry: "Das habe ich schon immer so gemacht. Wieso sollte ich es ändern?").

Nach wenigen Tagen melden Kunden erste Probleme, die Kollege Hilflos unter hohem Stress zu beseitigen hat (vermutlich abermals nur mangelhaft getestet). Es ist nur eine Frage der Zeit, bis das nächste Problem auftaucht und kundenseits Unzufriedenheit, developerseitig Stress verursacht. (Harry: "Stress gehört halt zum Job. Ist unvermeidlich.").

Also Zufall hoch drei. Wenn die Wahrscheinlichkeit, mit einer Testsystematik Fehler finden zu können, gegen null geht, dann ist die Testsystematik ungeeignet, denn es wird de facto gar nicht getestet. In zufällig ausgewählten Teilergebnissen, die aus abermals zufällig ausgewählten Testdaten generiert wurden, systematisch Fehler finden zu wollen, ist grenzwertig unwahrscheinlich. Zufall * Zufall * Zufall: Harry, spiele lieber Lotto.

Testen? Dokumentation? Keine Zeit, ich will Ergebnisse sehen!

Das vorgenannte Beispiel ist konstruiert, ja, aber wohl nicht weit entfernt von der Realität. In mancher Softwareabteilung sind systematische Test- und Dokumentationsverfahren von vornherein entweder nicht eingeplant oder sie fallen einer zu knapp bemessenen Projektdauer zum Opfer. Die Deadline rückt näher, das Programm ist noch nicht fertig, also: Ablieferung ohne Doku und ohne Testen.

Die vermeintliche Zeitersparnis in der Neuentwicklung ist folgenreich, weil der tatsächliche Aufwand durch spätere Bugfixings wesentlich höher ist. Dass infolgedessen andere Projekte nur zeitverzögert begonnen werden können (oder unterbrochen werden müssen, weil Fehlerkorrekturen in produktiven Altprojekten dringender sind), kommt erschwerend hinzu. Testszenarien, die in der Entwicklungsphase nicht angelegt werden, belasten die Maintenance-Phase überproportional.

Damit einher geht oft noch eine mangelhafte oder völlig fehlende Dokumentation. Das wird schwierig, wenn derjenige Developer, der bei einem akuten Produktivfehler "Feuerwehr" spielen soll, nicht derselbe ist, der das Programm ursprünglich geschrieben hat, und sich unter hohem Druck erst in dessen Funktionsweise einarbeiten muss. Einige wenige Kommentarzeilen (für die der ursprüngliche Developer wenige Minuten benötigt) hätten helfen können, kostbare Zeit bei der Recherche (gleichbedeutend mit der Zeitverzögerung / dem Produktionsausfall, bis das Programm wieder läuft) sowie eine erhebliche Stressbelastung bei dem Akuthelfer zu sparen.

Auf systematische Tests und Dokumentation wird im Projektverlauf auch gern verzichtet, wenn der Leistungsaufwand für komplexe Arbeitspakete zu Anfang falsch eingeschätzt wird. Softwareprojekte, die von vornherein auf einen Fixtermin kalkuliert sind, "inklusive Tests und Dokumentation", dauern dann unvorhergesehen länger. Mit zunehmender Projektdauer wird klar, dass der Liefertermin gefährdet ist. Schrittweise werden dann eingeplante Leistungen verkürzt oder entfallen ganz; erste Kandidaten sind hierfür systematische Tests und Dokumentation.

Wenn das zuständige Management das zulässt, dann fallen wesentliche Hürden für Qualitätsarbeit. Testen ist nur sinnvoll mit gemeinsamen Qualitätsstandards, die für das ganze Team zu gelten haben. Wenn nur einzelne Entwickler freiwillig testen, der Rest des Teams sich jedoch dagegen sperrt, dann macht es keinen Sinn. Die Notwendigkeit für systematisches Testen wird häufig erst dann erkannt, wenn etwas vorgefallen ist, das teure Konsequenzen nach sich zog.

Unterschiedliche Konfigurationen

Im Ergebnis führen knapp kalkulierte Zeitpuffer dazu, dass zu wenig getestete Programmteile in hoher Eile in die Produktivumgebung übernommen werden. Von einem Test, wie all diese Programmteile in der Produktivumgebung zusammenspielen, ist oft keine Rede. Denn nicht immer entspricht die Konfiguration der Testumgebung jener der Live-Umgebung.

Angenommen, Sie haben drei verschiedene Umgebungen, in denen die Programmierarbeit stattfindet: die lokale Developer-Umgebung, die Test-Umgebung, und die Live-Umgebung. Und alle drei sind grundverschieden konfiguriert. Die Developer- sowie die Test-Umgebung ist meinetwegen Windows-basiert auf Java 1.8, die Live-Umgebung läuft auf einem Unix- oder Linux-System unter Java 1.6. Dass obendrein unterschiedliche XSL-Prozessoren zum Einsatz kommen, entschärft die Problematik nicht wirklich.

Sind einige wenige zufallsbasierte Tests in der Testumgebung mit optischer Kontrolle zufällig ausgewählter Ergebnisdetails an sich schon fragwürdig, so geht die Aussagekraft dieser Tests vollends gegen null, wenn die Konfiguration der Produktivumgebung nicht jener der Testumgebung entspricht. Wie aber ist so etwas überhaupt möglich?

Das kann vorkommen, wenn der Testserver gleichzeitig als Enwicklungsumgebung für geplante Änderungen am Live-System dient. Etwa, wenn (entgegen des bewährten Grundsatzes "Never change a running system") eine ganze Abteilung von Systemadministratoren unentwegt an beiden Systemen parallel herumschraubt: am Testsystem, um geplante Änderungen am Live-System auszutesten, und am Produktivsystem, um unter extrem hohem Stress und Erfolgsdruck entstandene Fehler auszubügeln.

Ich kann verstehen, dass man eine Umgebung zum Experimentieren benötigt. Sie ist erforderlich, um geplante Änderungen auf den produktiven Live-Servern zu entwickeln und auch zu testen. Erweiterte Bibliotheken, neue Technologien, Optimierung der Prozesse: Das alles muss sorgfältig entwickelt und auch getestet werden. Dazu gehört auch, solide auszutesten, was passiert, wenn man die aktuellen (derzeit produktiven) Programme in die neue, geplante Umgebung portiert.

Hier sind Probleme zu erwarten, und diese müssen durch entsprechende Korrekturen an den Programmen beseitigt werden. Das kann nicht auf dem Produktiv-Server geschehen, denn der aktuelle Live-Betrieb darf durch diese Experimente nicht berührt werden. Heißt: Die Konfiguration des Produktiv-Servers bleibt unangetastet, bis eine auf dem Testserver sorgsam durchgeprüfte Änderung übernommen wird.

Developer kümmern sich üblicherweise um jene Programme, die letztlich auf den Live-Servern laufen sollen. Vorher müssen sie sie unter entsprechenden Live-Bedingungen testen. Dafür benötigen sie zwingend eine Test-Umgebung, deren Konfiguration exakt jener des Produktiv-Servers entspricht. Insofern ist jede Abweichung zwischen Test- und Produktivserver als kardinaler Fehler anzusehen.

Die (Live-identische) Test-Umgebung mag in diesem Fall auch für weitergehende Tests wie Last- oder Abnahmetests verwendet werden. Das sind ebenfalls wichtige Tests, die nicht auf dem eigentlichen Live-Server durchgeführt werden sollten, denn dieser sollte für Tests jeglicher Art tabu sein.

White-Box-Tests

Grundsätzlich unterscheidet man beim Testen zwischen "white box"- und "black box"- Tests.

Black-Box-Tests werden in der Regel ohne Kenntnis des Programmcodes durchgeführt. Getestet wird hier insbesondere, ob die Spezifikation erfüllt ist. Zuständig sind spezielle Tester, also nicht der Entwickler.

White-Box-Tests beziehen sich dagegen unmittelbar auf den Programmcode, um sicherzustellen, dass keine Fehler auftreten. Getestet wird insbesondere, dass

Zuständig für die Durchführung der White-Box-Tests ist in der Regel der Entwickler, der die innere Funktionsweise des Programms am besten kennt. Diese Tests sollten gerichtstauglich dokumentiert werden.

Insgesamt geht es darum, gezielt Fehler zu provozieren und zu prüfen, ob diese in der Programmierlogik abgefangen werden. Ein typischer Programmierfehler ist der sorglose Umgang mit XML-Input-Elementen, deren Inhalte im Output zwingend (mandantory) vorhanden sein müssen. Hier können Sie mit XSLT einen Testfall automatisch generieren, indem Sie aus einem Input-Dokument sämtliche optionalen Felder entfernen.

Weitere typische Programmierfehler finden sich bei automatischen oder internen . Auch hierfür sollten Sie geeignete Testfälle vorsehen (immer im Bewußtsein, dass die Testszenarien fehlerhaft oder unzureichend sein können).

Sowohl bei der Variation der Testfälle als auch bei der Ergebniskontrolle kann Automatisierung helfen. Das Kernprinzip des automatisierten Testings liegt im automatisierten Abgleich von Soll- und Istwerten, verbunden mit einem Fehlerreport.

wg / 14. April 2018



Fragen? Anmerkungen? Tips?

Bitte nehmen Sie Kontakt zu mir auf.




Selenium



Vielen Dank für Ihr Interesse an meiner Arbeit.


V.i.S.d.P.: Wilfried Grupe * Klus 6 * 37643 Negenborn

☎ 0151. 750 360 61 * eMail: info10@wilfried-grupe.de

www.wilfried-grupe.de/testing.html