image
image

Andreas Spillner ist Professor für Informatik an der Hochschule Bremen. Er war über 10 Jahre Sprecher der Fachgruppe TAV »Test, Analyse und Verifikation von Software« der Gesellschaft für Informatik e.V. (GI) und bis Ende 2009 Mitglied im German Testing Board e.V. Im Jahr 2007 ist er zum Fellow der GI ernannt worden. Seine Arbeitsschwerpunkte liegen im Bereich Softwaretechnik, Qualitätssicherung und Testen.

image

Ulrich Breymann war als Systemanalytiker und Projektleiter in der Industrie und der Raumfahrttechnik tätig. Danach lehrte er als Professor Informatik an der Hochschule Bremen. Er arbeitete an dem ersten C++-Standard mit und ist ein renommierter Autor zu den Themen Programmierung in C++, C++ Standard Template Library (STL) und Java ME (Micro Edition).

Image

Zu diesem Buch – sowie zu vielen weiteren dpunkt.büchern – können Sie auch das entsprechende E-Book im PDF-Format herunterladen. Werden Sie dazu einfach Mitglied bei dpunkt.plus+:

www.dpunkt.de/plus

Lean Testing für C++-Programmierer

Angemessen statt aufwendig testen

Andreas Spillner
Ulrich Breymann

image

Prof. Dr. Andreas Spillner

Andreas.Spillner@hs-bremen.de

Prof. Dr. Ulrich Breymann

breymann@hs-bremen.de

http://leantesting.de

Lektorat: Christa Preisendanz

Copy-Editing: Ursula Zimpfer, Herrenberg

Satz: die Autoren mit LaTeX

Herstellung: Susanne Bröckelmann

Umschlaggestaltung: Helmut Kraus, www.exclam.de

Druck und Bindung: M.P. Media-Print Informationstechnologie GmbH, 33100 Paderborn

Bibliografische Information der Deutschen Nationalbibliothek

Die Deutsche Nationalbibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detaillierte bibliografische Daten sind im Internet über http://dnb.d-nb.de abrufbar.

ISBN:

Print   978-3-86490-308-3

PDF   978-3-86491-967-1

ePub   978-3-86491-968-8

mobi   978-3-86491-969-5

1. Auflage 2016

Copyright © 2016 dpunkt.verlag GmbH

Wieblinger Weg 17

69123 Heidelberg

Die vorliegende Publikation ist urheberrechtlich geschützt. Alle Rechte vorbehalten. Die Verwendung der Texte und Abbildungen, auch auszugsweise, ist ohne die schriftliche Zustimmung des Verlags urheberrechtswidrig und daher strafbar. Dies gilt insbesondere für die Vervielfältigung, Übersetzung oder die Verwendung in elektronischen Systemen.

Es wird darauf hingewiesen, dass die im Buch verwendeten Soft- und Hardware-Bezeichnungen sowie Markennamen und Produktbezeichnungen der jeweiligen Firmen im Allgemeinen warenzeichen-, marken- oder patentrechtlichem Schutz unterliegen.

Alle Angaben und Programme in diesem Buch wurden mit größter Sorgfalt kontrolliert. Weder Autor noch Verlag können jedoch für Schäden haftbar gemacht werden, die in Zusammenhang mit der Verwendung dieses Buches stehen.

5 4 3 2 1 0

Vorwort

Liebe Leserinnen und Leser,

es ist das Ziel eines jeden Softwareentwicklers1, Programme mit möglichst wenigen Fehlern zu schreiben. Wie man weiß, ist das weiter gehende Ziel einer fehlerfreien Software nicht zu erreichen, von sehr kleinen Programmen abgesehen. Es ist aber möglich, die Anzahl der Fehler zu reduzieren. Dabei helfen erstens konstruktive Maßnahmen. Dazu gehört die Einhaltung von Programmierrichtlinien ebenso wie das Schreiben eines verständlichen Programmtextes. Zweitens hilft das Testen, also die Prüfung der Software, ob sie den Anforderungen genügt und ob sie Fehler enthält.

Die beim Testen häufig auftretende Frage ist, wie viel Aufwand in einen Test gesteckt werden soll. Einerseits möglichst wenig, um die Kosten niedrig zu halten, andererseits möglichst viel, um dem Ziel der Fehlerfreiheit nahezukommen. Letztlich geht es darum, einen vernünftigen Kompromiss zwischen diesen beiden Extremen zu finden. Der Begriff »lean« im Buchtitel bedeutet, sich auf das Wichtige zu konzentrieren, um diesen Kompromiss zu erreichen. Die Frage des Aufwands ist aber nur vordergründig ausschließlich für Tester von Bedeutung.

Tatsächlich checkt ein Softwareentwickler seinen Code erst ein, wenn er ihn auf seiner Ebene, also der Ebene der Komponente oder Unit, getestet hat. Er ist interessiert an der Ablieferung guter Software und an der Anerkennung dafür. Er muss aber auch darauf achten, nicht mehr Zeit als angemessen zu investieren. Dieses Buch soll eine Brücke zwischen Programmierung und Testen für den C++-Entwickler bauen und ihm zeigen, welche Testverfahren es gibt und wie sie mit vertretbarem Aufwand auf seiner Ebene eingesetzt werden können.

Zum fachlichen Hintergrund der Autoren: Ulrich Breymann ist mit seinem Standardwerk »Der C++-Programmierer« [Breymann 15] in C++-Programmierer-Kreisen bekannt. Damit lernen Leser, wie sie in C++ programmieren können und dabei durch guten Programmierstil Qualität in ihre Programme bekommen. Andreas Spillner hat mit »Basiswissen Softwaretest« [Spillner & Linz 12] im Bereich des Testens ebenfalls ein grundlegendes Buch geschrieben. Der Inhalt seines Buches orientiert sich am internationalen Lehrplan »Certified Tester – Foundation Level« und umfasst neben einigen der hier aufgeführten Testverfahren noch weitere Themen.

Das vorliegende Buch zeigt die praktische Anwendung der Testverfahren für C++-Programme mit zahlreichen ausführlichen Beispielen. Dabei liegt der Fokus auf »Lean Testing«, also dem Versuch, einen guten Kompromiss zwischen angestrebter Qualität und Testaufwand zu finden.

Wir hoffen, Ihnen beim Durcharbeiten der folgenden Kapitel viele Hinweise und Anregungen für den Test Ihrer Software als Teil der täglichen Arbeit geben zu können.

Unserer Lektorin Frau Preisendanz und dem dpunkt-Team danken wir für die sehr gute Zusammenarbeit.

Bremen, im April 2016

Andreas Spillner & Ulrich Breymann

Inhaltsverzeichnis

1 Einleitung

2 Test gegen die Anforderungen

3 Statische Verfahren

3.1 Codereview

3.2 Compiler

3.3 Analysewerkzeuge

3.4 Analysebeispiele

3.4.1 Clang als Analysewerkzeug

3.4.2 Scan-Build

4 Testentwurf und Testdurchführung

4.1 Das Google-Test-Framework

4.1.1 Installation

4.1.2 Anwendung

4.2 Happy-Path-Test

4.3 Äquivalenzklassentest

4.3.1 Ein Beispiel mit einem Parameter

4.3.2 Das Beispiel in C++

4.3.3 Erweiterung auf andere Datentypen

4.3.4 Mehrere Parameter

4.4 Grenzwertanalyse

4.4.1 Ein Beispiel

4.4.2 Mehrere Parameter

4.4.3 Ergänzung: Grenzen im Programmtext

4.5 Klassifikationsbaummethode

4.5.1 Ein Beispiel

4.5.2 Das Beispiel in C++

4.6 Kombinatorisches Testen

4.6.1 Orthogonale Arrays

4.6.2 Covering Arrays

4.6.3 n-weises Testen

4.6.4 Werkzeugnutzung

4.6.5 Das Beispiel in C++

4.6.6 Ein Beispiel ohne Orakel

4.7 Entscheidungstabellentest

4.7.1 Ein Beispiel

4.7.2 Ein Beispiel in C++

4.8 Zustandsbasierter Test

4.8.1 Ein Beispiel

4.8.2 Der minimale Zustandstest

4.8.3 Das Beispiel in C++

4.8.4 Test von Übergangsfolgen

4.9 Syntaxtest

4.9.1 Das Beispiel in C++ – Variante 1

4.9.2 Das Beispiel in C++ – Variante 2

4.10 Zufallstest

5 Strukturbasierte Testverfahren

5.1 Kontrollflussbasierter Test

5.1.1 Werkzeugunterstützung

5.1.2 Anweisungstest

5.1.3 Entscheidungstest

5.1.4 Pfadtest

5.1.5 Schleifentest

5.2 Test komplexer Entscheidungen

5.2.1 Einfacher Bedingungstest

5.2.2 Mehrfachbedingungs- oder Bedingungskombinationstest

5.2.3 Modifizierter Bedingungs-/Entscheidungstest

5.3 Bewertung

5.4 Bezug zu anderen Testverfahren

5.5 Hinweise für die Praxis

6 Erfahrungsbasiertes Testen

6.1 Exploratives Testen

6.2 Freies Testen

7 Softwareteststandard ISO 29119

7.1 Testverfahren nach ISO 29119

7.1.1 Spezifikationsbasierte Testverfahren

7.1.2 Strukturbasierte Testverfahren

7.1.3 Erfahrungsbasierte Testverfahren

8 Ein Leitfaden zum Einsatz der Testverfahren

9 Zu berücksichtigende C++-Eigenschaften

9.1 Automatische Typumwandlung

9.2 Undefinierte Bitbreite

9.3 Alignment

9.4 32- oder 64-Bit-System?

9.5 static-Missverständnis

9.6 Memory Leaks

Glossar

Literaturverzeichnis

Stichwortverzeichnis

1 Einleitung

Worum geht’s

Kommt Ihnen die folgende Situation bekannt vor? Die User Story (das Feature, die Klasse, das Anforderungsdetail) ist fertig programmiert und bereit zum Einchecken für den Continuous-Integration-Prozess. Ich habe alles sorgfältig bedacht und ordentlich programmiert, aber bevor ich den Code einchecke, möchte ich noch meinen Systemteil testen. Es wäre ja zu ärgerlich, wenn es später Fehler gibt, deren Ursache in meinem Teil liegt; muss ja nicht sein! Also los geht’s mit dem Testen. Aber wie und womit fange ich an und wann habe ich ausreichend genug getestet?

Genau hierfür gibt das Buch Hinweise! Es beantwortet die Fragen: Wie erstelle ich Testfälle1? Welche Kriterien helfen, ab wann ein Test als ausreichend angesehen und damit beendet werden kann? Wir meinen, dass jeder Entwickler auch testet, zumindest seinen eigenen Programmcode, wie in der oben beschriebenen Situation. Und wenn der Entwickler dann praktische Hinweise in seiner vertrauten Programmiersprache, hier C++, bekommt, so hoffen wir, dass es zur Verbesserung des Testvorgehens bei ihm führt und ihm bei seiner täglichen Arbeit hilft.

Wie wäre es mit folgendem Ablauf: Die User Story, das Feature, die Klasse, das Anforderungsdetail ist fertig programmiert und vor dem Einchecken für den Continuous-Integration-Prozess erfolgten die Schritte:

Das Buch beschränkt sich auf den sogenannten Entwicklertest, also den Test, den der Entwickler direkt nach der Programmierung durchführt. Andere geläufige Bezeichnungen sind Unit Test, Komponententest oder Modul-test, um nur einige zu nennen.

Es geht darum, die kleinste Einheit zu wählen, bei der es Sinn macht, einen separaten Test durchzuführen. Dies kann eine Methode oder Funktion einer Klasse sein oder auch eine Zusammenstellung von mehreren Klassen, die eng miteinander in Kommunikation stehen.

TDD (Test Driven Development)

Viele Autoren empfehlen die testgetriebene Entwicklung2 – zu Recht! Damit ist gemeint, dass zuerst die Testfälle auf Basis der Spezifikation entwickelt werden, und erst danach der damit zu testende Programmcode geschrieben wird. Dieses Buch konzentriert sich nicht auf TDD, aber fast alle der genannten Verfahren sind dafür sehr gut geeignet. Letztlich sind sie unabhängig davon, ob noch zu schreibender oder schon vorhandener Code damit getestet werden soll. Eine Ausnahme sind die Verfahren zur statischen Analyse von Programmen sowie die Whitebox-Tests, die vorhandenen Programmcode voraussetzen.

Test-Büffet

Wir sehen den Inhalt des Buches als »Test-Büffet«. Wie bei einem Büffet gibt es reichlich Auswahl und der Hungrige entscheidet, welche Wahl er trifft und wie viel er sich von jedem Angebot nimmt, auch wie viel Nachschlag er noch »verträgt«. Ähnlich ist es auch mit dem Testen: Es gibt nicht das eine Testverfahren, mit dem alle Fehler aufgedeckt werden; sinnvoll ist immer eine Kombination mehrerer Verfahren, die der Entwickler passend zum Problem aussucht. Wie intensiv und ausgiebig die einzelnen Verfahren anzuwenden sind – wie viel sich jeder vom Büffet von einer Speise auftut – ist ihm überlassen, er kennt sein Testobjekt – seinen Geschmack – am besten. Wir geben Empfehlungen, welche Reihenfolge anzuraten ist und welche Speisen in welchem Umfang gut zusammenpassen – ein Eis vorweg und danach fünf Schweineschnitzel und eine Karotte scheint uns keine ausgewogene Zusammenstellung.

Beim Büffet gibt es Vor- und Nachspeisen sowie Hauptgerichte. Ebenso verhält es sich beim Testen: Statische Analysen sind vor dem eigentlichen Testen besonders sinnvoll, die Haupttestverfahren sind die Verfahren, bei denen die Testfälle systematisch hergeleitet werden. Erfahrungsbasierte Verfahren runden das Menü ab. Eine ausgewogene Zusammenstellung ist die Kunst – nicht nur beim Büffet, sondern auch beim Testen. Nur mit Nachspeisen seinen Hunger zu stillen, ist sicherlich verlockend, aber rächt sich meist später. Ausschließlich auf die eigene Erfahrung beim Testen der eigenen Software zu setzen, birgt das Problem der Blindheit gegenüber den eigenen Fehlern. Wenn ich als Entwickler die User Story falsch interpretiert oder etwas nicht bedacht habe, werde ich, wenn ich meine Rolle als Entwickler mit der Rolle des Testers vertausche, nicht automatisch die Fehlinterpretation durch die korrekte in meinem Kopf ausgetauscht bekommen. Wenn ich aber systematische Testverfahren verwende, dann erhalte ich durch die Verfahren möglicherweise Testfälle, die mich auf meine Fehlinterpretation oder die nicht bedachte Lücke hinweisen oder mich zumindest zum Nachdenken animieren.

»Lean Testing«

Beim Büffet wird wohl keiner auf die Idee kommen, das Büffet komplett leer essen zu wollen. Uns scheint es, dass beim Testen aber ein ähnliches Bild in manchen Köpfen noch vorherrscht.

So schreibt Jeff Langr beispielsweise [Langr 13, S. 35]: »Using a testing technique, you would seek to exhaustively analyze the specification in question (and possibly the code) and devise tests that exhaustively cover the behavior.« Frei übersetzt: »Beim Testen versuchen Sie, die zugrunde liegende Spezifikation (und möglicherweise den Code) vollständig zu analysieren und Tests zu ersinnen, die das Verhalten vollständig abdecken.«3

Er verbindet das Testen mit dem Anspruch der Vollständigkeit. Dies ist aber unrealistisch und kann in der Praxis in aller Regel nie erfüllt werden.

Es ist aber auch gar nicht erforderlich, wenn einem bewusst ist, dass ein Programmsystem während seiner Einsatzzeit nie mit allen möglichen Kombinationen ausgeführt werden wird.

Ein kurzes Rechenbeispiel soll dieses veranschaulichen: Nehmen wir an, wir hätten ein sehr einfaches System, bei dem 3 ganze Zahlen einzugeben sind. Jede dieser Zahlen kann 216 unterschiedliche Werte annehmen, wenn wir von 16 Bit pro Zahl ausgehen. Bei Berücksichtigung aller Kombinationen ergeben sich dann 216 · 216 · 216 = 248 Möglichkeiten. Dies sind 281.474.976.710.656 unterschiedliche Kombinationen der drei Eingaben. Damit die Zahl greifbarer wird, nehmen wir an, dass in einer Sekunde 100.000 unterschiedliche Programmläufe durchgeführt werden. Nach 89,2 Jahren hätten wir jede mögliche Kombination einmal zur Ausführung gebracht. Bei 32 Bit pro Zahl ergäben sich sogar 2,5 · 1016 Jahre. Noch Fragen?

Es muss daher eine Beschränkung auf wenige Tests vorgenommen werden. Es gilt, einen vertretbaren und angemessenen Kompromiss zwischen Testaufwand und angestrebter Qualität zu finden. Dabei ist die Auswahl der Tests das Entscheidende! Eine Konzentration auf das Wesentliche, auf die Abläufe, die bei einem Fehler einen hohen Schaden verursachen, ist erforderlich.

Zu diesem Zweck gibt es Testverfahren, die eine Beschränkung auf bestimmte Testfälle vorschlagen. Wir haben unserem Buch den Titel »Lean Testing« gegeben, um genau diesen Aspekt hervorzuheben. Wir wollen dem Entwickler Hilfestellung geben, damit er die für sein Problem passenden Tests in einem angemessenen Zeitaufwand durchführen kann, um die geforderte Qualität mit den Tests nachzuweisen. Ein vollständiger Test wird nicht angestrebt. Wir wollen unser Essen vom Büffet beenden, wenn wir ausreichend gesättigt sind und eine für unseren Geschmack passende Auswahl von Speisen – nicht alle – probiert haben.

»Lean Testing« setzt »Lean Programming« voraus

Um mit wenig Testeinsatz viel überprüfen zu können, muss der Code – das Testobjekt – möglichst einfach sein. Trickreicher und »künstlerischer, freier« Programmierstil sind da nicht gewünscht. Aber glücklicherweise hat sich in den letzten Jahren ein Wandel hin zum einfachen guten Programmierstil ergeben.

Die Beachtung der »Clean-Code-Prinzipien« schafft eine wichtige Voraussetzung, den Test angemessen aufwendig gestalten zu können. Erst durch eine einfache Programmstruktur ist eine einfache Testbarkeit gegeben. Die einfache Testbarkeit garantiert, dass der Test mit einfachen Methoden und Ansätzen durchgeführt werden kann und damit »lean« ist. Auch Refactoring ist ein wichtiger Pfeiler für eine einfache Testbarkeit. Wenn der Code unübersichtlich wird, sind Vereinfachungen vorzunehmen. Listing 1.1 zeigt ein Beispiel für einen Programmcode, bei dem sich Refactoring lohnt:

// gibt Preis in Eurocent zurück
int fahrpreis(int g,                  // Grundpreis
              int c,                  // Preis pro KM
              int s,                  // Strecke
              bool n,                 // Nachtfahrt
              bool gp) {              // Gepäck ja/nein
  int b = c * s;                      // Basispreis
  int r = 0;                          // Rabatt
  int z = 0;                          // Zuschlag für Nachtfahrt
  if(s > 50)
    r = static_cast<int>(0.1 * b + 0.5);
  else if(s > 10)
    r = static_cast<int>(0.05 *  b + 0.5);
  if(n)
    z = static_cast<int>(0.2 * b + 0.5);
  if(gp)
    z += 300;                         // Zuschlag für Gepäck
  return  g + b + z - r;
}

Listing 1.1: »Unsauberer« Code

Die an diesem Listing zu kritisierenden Punkte sind:

  1. Die Variablennamen werden kommentiert, sind aber sehr kurz. Besser ist es, Namen zu verwenden, die die Kommentierung überflüssig machen. Wenn der Code beim Lesen über eine Seite geht, sind die Kommentare verschwunden, und es muss möglicherweise umständlich zurückgeblättert werden.

  2. Die Anweisungen nach den if-Bedingungen sind nicht in geschweifte Klammern eingeschlossen – eine mögliche Fehlerquelle, wenn die Anweisungen durch weitere ergänzt werden sollen.

  3. Dreimal wird static_cast verwendet. Die 0.5 deutet darauf hin, dass ein double-Wert gerundet werden soll. Besser wäre es, den Vorgang des Rundens in eine eigene Funktion mit geeignetem Namen auszulagern, damit beim Lesen klar wird, was geschehen soll. Anstelle einer eigenen Funktion eignet sich dafür die C++-Funktion std::round(). Im Beispiel fällt auf, dass die Rundung nur für positive Werte von b korrekt ist. std::round() rundet auch negative Werte korrekt.

  4. z wird zu Beginn deklariert, nicht kurz vor der Stelle der ersten Verwendung – das Lokalitätsprinzip wird verletzt.

Nach dem Refactoring könnte die Funktion so aussehen, wie sie in Listing 4.35 auf Seite 102 abgedruckt ist.

Beide Ansätze – Clean Code und Refactoring – sehen wir nicht nur im agilen Umfeld als sinnvoll an, ganz im Gegenteil: Einfache Programmierung ist in allen Bereichen und überall anzustreben.

Worum geht’s nicht

Auf einem Büffet, sei es auch noch so umfangreich oder von der Zusammenstellung her thematisch begrenzt (z.B. ein Fisch- oder Vegan-Büffet), sind nie alle möglichen Speisen zu finden. So verhält es sich auch mit diesem Buch. Folgendes wird nicht behandelt:

Qualität wird bei der Entwicklung von Software produziert. Mit Testen kann nur die erreichte Qualität nachgewiesen, aber nicht verbessert werden. Stichpunkte sind die Vermeidung von unsicheren Sprachkonstrukten, das defensive Programmieren, die Einhaltung der Clean-Code-Empfehlungen, für Testbarkeit des Programms zu sorgen und Robustheit zu schaffen, um nur einige Ansätze zu nennen. Zu all diesen wichtigen Punkten finden Sie nichts in diesem Buch, wir verweisen aber – wie auch bei den anderen Punkten – auf die entsprechende Literatur (siehe Anhang).

Wir setzen kein Vorgehensmodell der Softwareentwicklung voraus, da nach unserer Einschätzung Unit Tests durch die Entwickler in jedem Modell durchgeführt werden, auch wenn sie vom Modell her explizit gar nicht vorgeschrieben werden. Agiles Vorgehen und die Auswirkungen auf den Test werden daher ebenfalls nicht diskutiert. Test Driven Development sehen wir, wie inzwischen viele andere Autoren, nicht als Test-, sondern als Designkonzept und gehen darauf nicht näher ein.

Wie Testrahmen aufzubauen sind, damit das Testobjekt – Ihr programmiertes Stück Software, was getestet werden soll – überhaupt mit Testeingabedaten versorgt und ausgeführt werden kann, wird nur indirekt durch die Verwendung entsprechender Frameworks beschrieben. Wir nutzen im Buch Google Test [URL: googletest]. Werkzeuge zur Fehlerverwaltung (»bugtracker«) werden ausgeklammert.

Da Entwicklertests direkt nach der Programmierung folgen, werden die weiteren Teststufen wie Integrationstest, Systemtest, Abnahmetest, Akzeptanztest, die anschließend durchgeführt werden, im Buch nicht behandelt. Damit finden Sie auch zu GUI-Tests, Usability-Tests, Performanztests und weiteren Tests, die eher den höheren Teststufen zuzuordnen sind, keine Informationen in diesem Buch. Testprozesse sowie deren Bewertung und Verbesserung gehören ebenfalls nicht zum Fokus des Entwicklertests.

Der Test von parallelen bzw. nebenläufigen Programmen erfordert weitere Ansätze, die hier auch nicht behandelt werden. Wir beschränken uns auf sequenzielle Programme.

2 Test gegen die Anforderungen

Um beim Testen entscheiden zu können, ob ein fehlerhaftes Verhalten vorliegt oder nicht, werden entsprechende Informationen benötigt. Diese Informationen sind in den Anforderungen oder in der Spezifikation zu finden. Getestet wird somit immer »gegen« ein vorab festzulegendes Verhalten oder Ergebnis des Testobjekts. Anforderungen oder Spezifikationen enthalten nur ganz selten alle zu berücksichtigenden Informationen. Die fehlenden Festlegungen sind vom Programmierer zu ergänzen oder durch Rückfrage beim Kunden zu klären, was die bessere Option ist. Domain-Fachwissen und gesunder Menschenverstand sind sicherlich recht hilfreich dabei.

Als Programmierer kann man auch folgende Meinung vertreten: »Alles, was nicht spezifiziert ist, gehört nicht zu meinen Aufgaben und brauche ich nicht zu berücksichtigen. Schließlich bezahlt mir niemand die Extra-Arbeit. Und jedes Programmverhalten bei Übergabe eines nicht spezifizierten Wertes – ob Absturz oder fehlerhafte Berechnung – ist o.k.« Eine solche Einstellung ist nur bei wirklich unkritischen Programmen, wie beispielsweise einem Spiel auf dem Handy, tolerierbar. In allen anderen Fällen muss überlegt werden, wie vom Programm aus auf mögliche, auch nicht spezifizierte Eingaben zu reagieren ist.

Anforderungen müssen in überschaubare Aufgaben aufgeteilt werden, um diese dann in Programmtext umzusetzen. Für jeden dieser Programmteile sind dann entsprechende Vor- und Nachbedingungen zu spezifizieren.

Betrachten wir folgende Anforderung an eine Funktion: Eine Prozentzahl, die als positiver ganzzahliger Wert übergeben wird, soll als Text (String) umgeformt und ausgegeben werden. Beispiel: Die Zahl 13 soll umgeformt werden zu »dreizehn«. Es handelt sich hier um eine relativ einfache Aufgabe, für die es auch entsprechende Bibliotheken gibt, aber darauf kommt es in unserem Beispiel nicht an. Wir möchten hier folgende Frage diskutieren:

Kann sich der Programmierer also auf die Einhaltung der Anforderungen verlassen? Kann er sich im Beispiel ausschließlich auf die Umsetzung der ganzzahligen Werte zwischen 0 und 100, denn andere Werte ergeben als Prozentzahlen keinen Sinn, konzentrieren und dann auch nur diese Werte beim Testen berücksichtigen?

Design by Contract

Eine sinnvolle und in der Praxis durchaus übliche Vorgehensweise zur Klärung des Problems ist das Prinzip »Design by Contract« [Meyer 13]. Es wird in einer Art Vertrag festgelegt, wofür der Aufrufer, der Dienstnehmer, verantwortlich ist und mit welchen Ergebnissen er vom Dienstanbieter nach dem Aufruf rechnen kann. Der Dienstanbieter kann eine größere Komponente, aber auch nur eine einfache Funktion sein. Vom Aufrufer sind die vereinbarten Vorbedingungen einzuhalten, der Dienstanbieter garantiert die Einhaltung der Nachbedingung. In unserem Beispiel wäre im Vertrag festzulegen, dass der Aufrufer garantiert, dass nur ganzzahlige Werte zwischen 0 und 100 (inklusive der beiden Werte) als Parameter übergeben werden. Der Dienstanbieter garantiert für diesen Fall, dass ein entsprechender String als Ergebnis zurückgegeben wird.

In der Praxis: Robustheit erwünscht

»Design by Contract« sagt nichts darüber aus, wie das Ergebnis aussieht, wenn die Vorbedingung nicht eingehalten wird. In der Praxis ist oft Robustheit erwünscht, d.h., dass Fehler möglichst abgefangen werden. Das bedeutet, dass die Vorbedingung geprüft wird, entweder vom Aufrufer oder vom Dienstanbieter. Vordergründig betrachtet ist die Konzentration der Prüfung an nur einer Stelle, dem Dienstanbieter, sinnvoll. Das ist aber nicht immer möglich. Dazu zwei Beispiele mit einfachen Funktionen:

  1. Eine Funktion double sqrt(double arg) soll die Quadratwurzel der Zahl arg zurückgeben. Vorbedingung sei, dass arg nicht negativ ist. In diesem Fall kann die Vorbedingung in der Funktion leicht geprüft und bei einem negativen Argument eine Exception geworfen werden.

  2. Eine Funktion bool binary_search(Iterator first, Iterator last, const T& value) soll zurückgeben, ob der Wert value im Bereich [first, last)1 enthalten ist. first und last sind Iteratoren, die in einen Container oder ein Array verweisen. Die Anzahl der Elemente im zu durchsuchenden Bereich sei mit n = last - first abgekürzt. Wesentlicher Vorteil des bekannten Algorithmus zur binären Suche ist seine Schnelligkeit. Er benötigt nur etwa log2n Schritte. Die Vorbedingung ist jedoch, dass der Bereich zwischen first und last sortiert ist. Wenn diese Vorbedingung nicht erfüllt ist, ist das Ergebnis undefiniert. In diesem Fall wäre die Prüfung der Vorbedingung innerhalb der Funktion nur theoretisch möglich: Sie würde nämlich n Schritte erfordern, sodass der eigentliche Vorteil des Algorithmus, seine Schnelligkeit, dahin wäre und man den Bereich ebenso gut linear durchsuchen könnte. Die Prüfung der Vorbedingung ist in diesem Fall also nicht sinnvoll.

Auswirkungen auf den Test

Die im Buch vorgestellten Testverfahren berücksichtigen nicht, ob die Software nach »Design by Contract« strukturiert und aufgeteilt ist oder nicht. Die Verfahren unterstützen die systematische Herleitung von Testfällen. Im obigen Beispiel der Umformung einer Zahl zwischen 0 und 100 in einen Text verlangt ein systematischer Test auch die Prüfung mit ganzzahligen Werten kleiner als 0 und größer als 100. Auch ist zu prüfen, wie sich das Programm bei fehlerhaften Eingaben verhält (double, float, negative int-Werte statt unsigned...). Da bei »Design by Contract« der Aufrufer für die Einhaltung der Vorbedingung verantwortlich ist, muss der Test mit den »falschen« Werten an jeder Aufrufstelle durchgeführt werden. Der Test beim Dienstanbieter kann sich dann darauf beschränken, dass nur ganzzahlige Werte zwischen 0 und 100 an der Schnittstelle übergeben werden. Die Fehlerbehandlung bei falschen Werten obliegt somit dem Aufrufer.

»Design by Contract« vereinfacht den Test beim Dienstanbieter, verlagert aber die Prüfung der Einhaltung der Vorbedingung an jede Aufrufstelle. Hier sind gegebenenfalls die falschen Werte abzufangen und mit einer aussagekräftigen Fehlermeldung abzulehnen. Diese Aufteilung muss jedem Programmierer bewusst sein, um seine Testaktivitäten entsprechend zu fokussieren. Ohne die Vereinbarung von Vor- und Nachbedingungen wäre im Beispiel die Prüfung der Einhaltung der Spezifikation des Parameterwertes Aufgabe der Funktion selbst, hier wären dann alle Überprüfungen zu programmieren.

3 Statische Verfahren

Der Begriff Software umfasst vieles. So gehören sowohl die Design- als auch die Programmdokumentation dazu und natürlich auch der Programmcode. Nur dieser wird im Folgenden betrachtet. Statische Verfahren analysieren den Programmcode, ohne ihn auszuführen – daher der Name. Zu den statischen Verfahren gehören sowohl Reviews zur Bewertung des Programmcodes und zur Aufdeckung von Fehlern als auch die automatisierte Analyse mit Werkzeugen. Es gibt verschiedene Arten von Reviews, auf die unten kurz eingegangen wird. Allen Reviews ist gemeinsam, dass sie Arbeitszeit kosten, nicht nur die des Autors, sondern auch die von Kollegen, die als Gutachter tätig werden. Uns geht es darum, die Arbeitszeit aller Beteiligten zu reduzieren, um mit demselben Aufwand bessere Qualität zu erzielen. Deshalb liegt der Schwerpunkt dieses Kapitels nicht auf Reviews, die nur der Vollständigkeit halber erwähnt werden, sondern auf Verfahren zur statischen Analyse, die automatisiert ablaufen und daher wenig Arbeitszeit kosten. Reviews werden dadurch nicht überflüssig, aber weniger aufwendig, weil ein Teil der Fehler oder Schwächen schon vorher durch die statische Analyse aufgedeckt und anschließend korrigiert werden kann. Je früher ein Fehler gefunden wird, desto leichter (und billiger) ist seine Korrektur.

Statische Verfahren werden typischerweise von Entwicklern eingesetzt. Diese Verfahren können natürlich nicht alle Fehler finden, insbesondere nicht diejenigen, die von externen Daten herrühren, die erst zur Laufzeit vom Programm eingelesen werden. Dafür gibt es die dynamischen Verfahren1, insbesondere die Unit Tests auf der Ebene des Entwicklers. Umgekehrt können Unit Tests verschiedene statische Eigenschaften nicht prüfen, wie etwa die Einhaltung von Programmierrichtlinien. Statische Verfahren sind besonders geeignet zur Prüfung der folgenden Elemente:

Konkrete Beispiele finden Sie weiter unten.

Statische Verfahren können nicht nur auf den Quellcode angewendet werden, sondern auch auf Bytecode (wie etwa das Werkzeug FindBugs2 für die Programmiersprache Java) oder auf binäre ausführbare Programme. Wir beschränken uns hier auf den Quellcode.

Beschränkungen in der Praxis

Ein Werkzeug ist ein Werkzeug, nicht mehr und nicht weniger. Insbesondere kann es nicht die Gedanken des Programmierers lesen und daraufhin die korrekte Umsetzung in Programmcode überprüfen. Das bedeutet, dass die Entwickler eines Werkzeugs bestimmte Vorstellungen haben, wie Anweisungen und Programmstrukturen aussehen sollen, und diese Annahmen im Werkzeug implementieren. Diese Annahmen können sich an manchen Stellen als falsch erweisen. Aus diesem Grund kann es sein, dass ein Werkzeug

Ein besonders gründliches Werkzeug erzeugt möglicherweise eine Menge falsch positiver Meldungen, sodass die Gefahr besteht, das wichtige Meldungen in der schieren Menge untergehen. Die Entwickler solcher Werkzeuge bemühen sich, die Anzahl der falsch positiven Ergebnisse zu reduzieren.

Praktischer Einsatz

Um die Menge an Fehlermeldungen und Warnungen zu reduzieren und die Anzahl der Tests zu beschränken, empfiehlt sich die nachstehende Reihenfolge:

  1. Das Testobjekt (Programmcode) compilieren und ggf. korrigieren, bis die Compilation fehlerlos durchläuft. Dabei die höchste Warnstufe einschalten und die Warnungen des Compilers berücksichtigen.

  2. Statische Verfahren einsetzen und alle gefundenen Fehler korrigieren. Die Anzahl der jetzt noch notwendigen Tests wird durch jeden in dieser Phase gefundenen Fehler reduziert.

  3. Erst dann die Unit Tests durchführen.

3.1 Codereview

Review ist der Oberbegriff für verschiedene statische Prüfverfahren, die von Personen durchgeführt werden. Das Prüfobjekt kann eine Designdokumentation sein, ein zu erstellendes Produkt oder ein Teil davon oder auch der Ablauf eines Prozesses. Ein Autor ist oft »betriebsblind« und sieht bestimmte Dinge nicht mehr, deshalb ist es wichtig, dass er den Code einem Kollegen zeigt. Ein großer Vorteil eines Reviews: Andere Personen sehen das Prüfobjekt unter einem ganz anderen Blickwinkel. In diesem Abschnitt geht es aber nur um Codereviews, die hauptsächlich in zwei Arten vorkommen:

Walkthrough

Das Vorgehen ist für kleine Teams von bis zu fünf Personen geeignet und verursacht relativ wenig Aufwand. Dabei stellt der Programmautor den Code einigen Experten vor, zum Beispiel fachlich versierten Kollegen – möglichst aus anderen Projekten – oder Testern. Mit ihnen zusammen werden verschiedene Benutzungsabläufe durchgespielt. Ziel ist das gegenseitige Lernen und Verständnis über das Prüfobjekt und natürlich, Fehler zu finden.

Inspektion

Eine Inspektion folgt einem formalisierten Ablauf, in dem es verschiedene Rollen gibt, wie etwa einen Moderator und verschiedenen Gutachter (ebenfalls fachlich versierte Kollegen). Einer der Gutachter trägt den Inhalt des Prüfobjekts vor, wobei die anderen entsprechend Fragen stellen. Die Gutachter bereiten sich auf die Sitzung vor, was beim Walkthrough entfallen kann. Während der Inspektion werden auch Daten gesammelt, die zur Qualitätsbeurteilung des Entwicklungs- und Inspektionsprozesses herangezogen werden. Ziel der Inspektion ist das Finden von Fehlern und ggf. deren Ursachen.

Ausführliche Beschreibungen zu den unterschiedlichen Arten der Reviews sind in [Spillner & Linz 12, Kap. 4] und in [Rösler et al. 13] zu finden.

3.2 Compiler

Ein Compiler muss zwangsläufig ein Programm analysieren, wenn er lauffähigen Code daraus herstellen soll. Typischerweise baut er einen abstrakten Syntaxbaum auf, abgekürzt AST (abstract syntax tree) genannt. Im Prinzip hat ein Compiler alle im Programmcode liegenden Informationen zur Verfügung, sodass er einige Fehler leicht entdecken kann, wie etwa die fehlende Deklaration einer Variablen oder den Aufruf einer Funktion mit einem falschen Parametertyp.

Was ein Compiler aber nicht oder nur zum Teil kennt, sind Empfehlungen für einen defensiven Programmierstil oder die gewünschten Programmierrichtlinien.

Es gibt eine Menge verschiedener Compiler, sowohl Open Source als auch kommerziell. Die hier und im Folgenden beschriebene Problematik ist bei allen Compilern strukturell ähnlich, weswegen wir uns auf die Open-Source-Compiler GNU C++ (G++) und Clang (Aufruf clang++) beschränken.

G++

G++ gibt es für Mac OS und Linux [URL: gcc]. Die Distribution für Mac OS hinkt bezüglich der Versionsnummern gegenüber Linux etwas hinterher. Unsere Empfehlung: Auf dem Mac lieber Clang verwenden (siehe unten). In beiden Fällen wird die mehrere Gigabytes umfassende Entwicklungsumgebung Xcode benötigt.

G++ gibt es auch für Windows, wobei die MinGW- und die Cygwin-Distribution am weitesten verbreitet sind. Aber auch hier hinken die Versionsnummern denen für Linux hinterher.

Clang

Clang [URL: clang] ist ein Frontend für die C-Sprachfamilie (C/C++, Objective C/C++), das für LLVM3, eine modulare Compilerarchitektur, entwickelt wurde. Clang/LLVM ist Open Source. LLVM ist aus einem Projekt der Compilerforschung entstanden. Clang/LLVM ist besser als G++ für die statische Analyse eines Programms geeignet und hat im Allgemeinen auch eine bessere Performanz. Clang ist als Ersatz für G++ konzipiert und akzeptiert daher dieselben und ähnliche Optionen. Clang wurde in Xcode integriert. Für Mac OS und Linux ist Clang sehr gut geeignet.

Windows wird ebenfalls unterstützt, allerdings beschränkt auf die Integration in Visual C++. Ein problemloses Zusammenwirken mit MinGW oder Cygwin ist derzeit nicht in Sicht (Clang nutzt Teile von Visual C++ bzw. MingW, arbeitet also nicht alleinstehend).

Warnungen des Compilers einschalten

Um die meisten Warnungen der genannten Compiler einzuschalten, wird die Option -Wall bei der Übersetzung übergeben. Aber trotz des Namens -Wall werden nicht alle Warnungen ausgegeben, mit -Wextra gibt es weitere Überprüfungen. Zusätzlich hilft die Option -pedantic, die compilerspezifische Erweiterungen meldet, die nicht dem C++-Sprachstandard entsprechen. -Wall und -Wextra umfassen jeweils viele Optionen. Bezüglich ihrer Beschreibung verweisen wir auf die Dokumentation zum Compiler. Clang stellt weitere Warn-Optionen bereit, die den Compiler als Analysewerkzeug nutzbar machen; mehr dazu erfahren Sie weiter unten. Sie müssen bei der Prüfung nicht das ausführbare Programm erzeugen lassen: Mit der Option -c wird das Linken weggelassen. Geben Sie allerdings nicht -fsyntax-only ein: Damit wird nur die Syntax geprüft und sonst nichts, d.h., es werden keine Templates instanziiert und daher auch nicht geprüft.

Nachstehend finden Sie einige Beispiele für (wahrscheinliche) Fehler, die ohne die genannten Optionen nicht gemeldet werden (verwendet wurden g++ 5.2 und Clang 3.8).

Im ersten Beispiel wird die Variable b der Variablen a zugewiesen, die dann als Bedingungsausdruck interpretiert wird:

if( a = b ) {
   // ...
}

Listing 3.1: Vermutlich fehlerhafter Vergleich

Dies ist legales C++. Vermutlich handelt es sich jedoch um einen Schreibfehler, der durch -Wall aufgedeckt wird. Die Warnung kann abgeschaltet werden, wenn es sich tatsächlich um eine Zuweisung und nicht um den Vergleich (a == b) handeln soll, indem ein weiteres Klammerpaar spendiert wird: if((a = b)). In unseren Augen ist dies allerdings schlechter Programmierstil: Besser ist es, Zuweisung und Vergleich in zwei Anweisungen zu zerlegen.

Leicht zu übersehen ist das überflüssige Semikolon in folgender Anweisung:

if ( a==b );
{
   // ...
}

Listing 3.2: Wirkungslose if-Abfrage

Es führt dazu, dass der Vergleich keine Wirkung hat, der Codeblock wird in jedem Fall ausgeführt. Dieser Fehler wird mit -Wall nicht entdeckt, aber mit -Wextra.

In der folgenden Anweisung ist eine Zahl versehentlich als int statt als double deklariert worden:

int z = 3.14;

Listing 3.3: Informationsverlust ohne Fehlermeldung

Diesen Fehler findet g++ trotz der eingeschalteten Optionen nicht. Clang meldet eine Warnung auch ohne die genannten Optionen. Guter Programmierstil ist es, solche Initialisierungen mit geschweiften Klammern zu schreiben:

int z {3.14};

Listing 3.4: Informationsverlust mit Fehlermeldung

Dann wird von jedem Compiler auch ohne Optionen als Fehler gemeldet, dass der Typ auf der rechten Seite auf den auf der linken Seite verengt wird. Das Umgekehrte (double z {3};) hingegen ist legal, weil kein Informationsverlust stattfindet.

Beim nächsten Beispiel ist ein Buffer-Overflow möglich:

void f(int a) {
  char buf[] = "Buffer-Overflow?";
  char c = buf[a];
  // ...
}

Listing 3.5: Möglicher Buffer-Overflow

Dieser potenzielle Fehler wird ohne die genannten Optionen nicht gemeldet. Allerdings wird nicht vor jedem Buffer-Overflow gewarnt: Die folgenden Zeilen werden klaglos übersetzt.

const char* name = "Georg Christoph Lichtenberg";
char buf[10];
std::strcpy(buf, name);

Listing 3.6: Buffer-Overflow

Also doch std::strncpy() oder noch besser std::string verwenden.

Aber es gibt noch bessere Möglichkeiten, wie der nächste Abschnitt zeigt.

3.3 Analysewerkzeuge