Zukunftsfähigkeit II – Korrektheit/Regressionsfreiheit

Ob Software schon und immer noch zukunftsfähig ist, wird im Review beurteilt. Den führt die Architektenrolle vor der Abnahme durch den Kunden umfassend durch. Geprüft wird auf

  • Korrektheit und
  • Wandelbarkeit

Beide Facetten der Zukunftsfähigkeit haben Aspekte, die es getrennt zu betrachten gilt. Bei der Korrektheit ist das zum einen die Maturität. Die ist vorhanden, wenn die Software schon bereit für die Abnahme/Auslieferung ist.

Zum anderen ist es die…

Regressionsfreiheit als Aspekt der Korrektheit

Software muss nicht nur schon korrekt sein in Bezug auf die aktuell umgesetzten Verhaltensanforderungen, sie muss auch noch korrekt sein in Bezug auf alle früheren.

Eine Regression liegt immer dann vor, wenn Software durch Veränderungen eine Qualität verliert, die sie bereits hatte. Je größer die Komplexität einer Codebasis, desto größer das Risiko für einen Rückfall auf ein vormaliges Entwicklungsniveau. Wenn die Codesituation unübersichtlich ist, dann ist „Verschlimmbessern“ eine ständige Gefahr. Gut gemeinte Verbesserungen an einer Stelle führen zu unbeabsichtigten Verschlechterungen an anderer. Wer hätte das als Anwender oder Entwickler nicht schon erlebt?

Regressionen sind schlimmer als Software, der die Maturität fehlt. Wenn neu hergestelltes Verhalten einen Fehler aufweist, dann ist das für Kunden nervig, aber halbwegs erwartet. So ist das eben mit Software… Das haben sie in vielen Jahren leidvoll gelernt.

Wenn jedoch Verhalten, das über Monate oder gar Jahre fehlerfrei war, auf einmal fehlerhaft ist, dann ist das ein Desaster. Hier wird dem Anwender der Boden unter den Füßen weggezogen. Er kann sich nicht mehr auf das verlassen, wozu er (entgegen allen Befürchtungen) Vertrauen gefasst hatte.

Regressionen führen zu einem schnellen tiefen Vertrauensverlust beim Kunden und werden regelmäßig eskaliert. Sie stören damit den ruhigen Fluss der Herstellung von Neuerungen erheblich. Nicht nur sind sie Nachbesserungen und daher Verschwendung, sie erfordern oft auch mehr Aufwand beim Aufbau eines mentalen Modells für die Korrektur, weil an den fehlerhaft gewordenen Stellen nicht aktuell gearbeitet wurde. Die Beziehung zum eigentlichen Arbeitsort war ja gerade nicht klar, sonst wäre keine Regression eingetreten.

Also: Regressionen sind zu vermeiden! Egal, was es kosten mag. Naja… fast ;-)

Das Mittel zur Herstellung von Regressionsfreiheit sind automatisierte Tests.

Während sich Maturität vielleicht noch mit manuellen Tests darstellen lässt, ist das für Regressionsfreiheit nicht der Fall. Manuelle Tests skalieren einfach nicht. Denn um zuzusichern, dass kein bisheriges Verhalten kompromittiert wurde durch aktuelle Veränderungen, müsste bisheriges Verhalten ja nochmals getestet werden.

Wenn die erste Iteration 3 Anforderungen umsetzt, sind Maturitätstests für 3 Anforderungen durchzuführen und 0 für die Regressionsfreiheit. Wenn dann jedoch die zweite Iteration 4 Anforderungen umsetzt, sind insgesamt 7 Tests durchzuführen: 4 für die Maturität und 3 für die Regressionsfreiheit. Bei der dritten Iteration mit 5 Anforderungen sind es 5+(4+3)=12 Tests, bei der vierten Iteration mit 2 Anforderungen 2+(5+4+3)=14 Tests usw. usf.

Wer konsequent Regressionsfreiheit überprüfen will, ist schon bald fast nur mit Regressionstests beschäftigt. Neue Anforderungen sind dann lediglich die Schaumkrone auf auf einer Welle in einem Ozean von Anforderungen, die alle, alle immer auch noch korrekt erfüllt sein wollen.

Ohne so konsequente Überprüfung von Regressionsfreiheit wird das Netz zum Fangen von Bugs mit jeder neuen Anforderungen weitmaschiger. Die Zahl der Tests im Verhältnis zur Anzahl der insgesamt umgesetzten Anforderungen nimmt ja stetig ab.

Ohne Automatisierung der Tests geht es also bei der Regressionsfreiheit nicht. Wer noch bei der Maturität meinte, ohne Automatisierung auszukommen, muss spätestens jetzt einsehen, dass das keine Option ist, wenn man die Zukunftsfähigkeit nicht leichtfertig aufs Spiel setzen will.

Wer bei der Maturität hingegen schon Ja zur Automatisierung gesagt hat, der hat ein Fundament für die Regressionsfreiheit gelegt. Die bleibenden Akzeptanztests spannen schon ein Netz auf, in dem sich Regressionen verfangen können.

Allerdings ist dieses Netz relativ weitmaschig. Je komplizierter die Domäne, desto weniger kann angenommen werden, dass Akzeptanztests genügend Pfade durch die verhaltenerzeugende Logik für das angestrebte Niveau von Regressionsfreiheit abdecken.

Der Review will für die Regressionsfreiheit also mehr bleibende Tests sehen als die Akzeptanztests. Die bilden allerdings das Fundament.

Aber was ist mit den Gerüsttests? Wäre es nicht doch gut, die zu behalten? Nein! Gerüsttests sind Gerüsttests und per definitionem zu löschen, nachdem sie geholfen haben, Maturität herzustellen. Sie testen ja Funktionen, die eigentlich privat sind und nicht unbedingt existieren. Sie gehören nicht zur Anforderungsdefinition des Kunden. Der ist lediglich an Eintrittspunkten interessiert, über die er Logik mit Input von außen triggern kann.

Anders liegt der Fall jedoch bei öffentlichen Methoden, die nicht vom Kunden gewünscht sind. Sie stellen Schnittstellen dar, über die Klassen von anderen benutzt werden können und sollen. Jede öffentliche Methode ist Teil eines Versprechens, nicht nur die an der Oberfläche einer Software.

Der Review überprüft daher im Hinblick auf Regressionssicherheit, ob alle öffentlichen Methoden mit angemessenen automatisierten Tests versehen und somit nachweisbar korrekt sind. Diese Tests lassen auch nachvollziehen, was sich die Entwickler dabei gedacht haben, Klassen zu definieren, die über das unmittelbar vom Kunden Gewünschte hinausgehen. Diese Klassen stellen ja eigenständige Verantwortungsbereiche dar. Insofern gibt es für sie ebenfalls Anforderungen und daher auch Akzeptanztests.

Software ist ein selbstähnliches Gebilde. So wie es sich nach außen darstellt – als ein Werkzeug, das Anforderungen erfüllt -, so ist sie innen selbst strukturiert: als eine Versammlung von Werkzeugen mit spezialisierteren Aufgaben.

Die äußere Form dieser Werkzeuge sind Module. Das sind Container für Logik, die einen Kontrakt definieren, der nach außen ein syntaktisches und ein semantisches Versprechen formuliert. „So kannst du mich benutzen. Das ist es, was ich tue.“

Der syntaktische Kontrakt besteht aus Funktionssignaturen. Der semantische Kontrakt ist unzweideutig beschrieben durch automatisierte Tests.

Funktionen sind die kleinsten Module. Klassen fassen Funktionen zu Modulen einer höheren Ebene zusammen. Bibliotheken wiederum fassen Klassen zusammen. Komponenten fassen Bibliotheken zusammen und Services Komponenten.

Auf die unterschiede der einzelnen Modulebenen soll an dieser Stelle nicht eingegangen werden. Für den Review der Regressionsfreiheit ist lediglich wichtig, dass jedes Modul auf jeder Ebene einen Kontrakt definiert, auf dessen korrekte Erfüllung Konsumenten der Module vertrauen.

Ultimativ ist das der Kontrakt gegenüber dem Anwender in Form einer Benutzerschnittstelle. Der ist allerdings vergleichsweise schlecht automatisiert zu testen. Es braucht dafür spezielle Werkzeuge und die Tests müssen besonders robust sein, da die Benutzerschnittstelle vielen Änderungen unterliegt, die nicht zwangsläufig zu Anpassungen von Tests führen sollen.

Doch schon kurz unter dieser Oberfläche lässt sich Logik in Modulen kapseln, die leicht zu testen sind, weil sie unabhängig von einer Benutzerschnittstellentechnologie sind. Dort erwartet der Review automatisierte Tests aller Modulkontrakte.

Wo also Methoden öffentlich sind, weil sie zu einem Kontrakt gehören, müssen ihnen Tests gegenüberstehen. Das ist eine konsequente Fortführung der Regel „Keine Anforderung ohne Akzeptanztest“.

Ob die Methoden geplant von vornherein öffentlich waren oder bei einer Refaktorisierung entstanden sind, ist unerheblich. Ein Akzeptanztest muss sein. Ja, sogar bei Refaktorisierungsergebnissen. Denn öffentliche Methoden sind, nun, öffentlich. Sie stellen ein Versprechen dar, das erstens explizit formuliert werden sollte (semantischer Kontrakt in Form eines Tests als Dokumentation) und zweitens immer wieder auf Einhaltung überprüft werden sollte (Regressionsfreiheit). Denn wenn alle Teile ihre Versprechen erfüllen, dann wird wohl auch das Ganze seine Versprechen erfüllen. Ein plausibler Gedanke, oder?

Aus dieser Richtung betrachtet werden auch nochmal Gerüsttests interessant. Sollte nämlich ein Gerüsttest einen Aspekt überprüfen, der nicht auch irgendwie in Akzeptanztests abgedeckt ist, dann gibt es nicht nur die Möglichkeit, die Akzeptanztests zu erweitern, um auch nach Löschen des Gerüsttests weiterhin den Aspekt im Blick zu behalten.

Die Alternative ist, diesen Aspekt, der bisher eigentlich ein unsichtbares Detail war, zu einem eigenständigen sichtbaren Teil aufzuwerten. Eine bisher eigentlich private Funktion wird dann öffentlich – und wandert in eine eigene Klasse. Sie ist damit Teil eines Kontraktes – und muss bleibend getestet werden. Zugehörige bisherige Gerüsttests werden damit quasi offiziell. Aus Gerüsten werden Pfeiler, d.h. tragende Strukturelemente.

Der Wunsch, „spannende“ Tests zu erhalten, ist mithin ein Treiber der Modularisierung von Software. Wenn Sie sich bisher gefragt haben, wie Sie Klassen „schneiden“ sollen, dann haben Sie jetzt ein weiteres Kriterium.

Es gehört daher nicht nur zum Review zu prüfen, ob Kontrakte unter Test stehen, sondern auch, wo noch Kontrakte definiert werden sollten. Durch Extraktion von Modulen werden dann Grenzen gezogen, die einerseits informieren, andererseits stabilisieren.

Sie informieren, weil sie Semantik beschreiben, die sonst undokumentiert wäre.

Sie stabilisieren, weil hinter einem Kontrakt das Innere eines Moduls sich unabhängig von der Umgebung entwickeln kann, solange der Kontrakt eingehalten wird.

Beispiel

Bezogen auf das Beispiel zur Maturität würde die Regressionsfreiheit noch eine weitere Option eröffnen. Sie erinnern sich? Es ging um Aggregationsfunktionalität: die Zahlen in einer Textdatei waren zu summieren. Nach dem Review der Maturität sah der Code so aus:

public class Aggregator {
    public int Sum(string filename) {
        var numbers = Load(filename);
        return Calculate_sum(numbers);
    }

    private int[] Load(string filename) {
        var lines = System.IO.File.ReadAllLines(filename);
        var numbers = new List<int>();
        for (var i = 0; i < lines.Length; i++)
            if (!string.IsNullOrWhiteSpace(lines[i]))
                numbers.Add(int.Parse(lines[i]));
        return numbers.ToArray();
    }

    private int Calculate_sum(IEnumerable<int> numbers) {
        var sum = 0;
        foreach (var n in numbers)
            sum += n;
        return sum;
    }
}

Aus einem Akzeptanztest und drei Gerüsttests für Load() und Calculate_sum() wurde ein erweiterter Akzeptanztest, der auch noch die Fälle der Gerüsttests von Load() abdeckt.

Das war eine legitime Entscheidung innerhalb des Horizonts Maturität. Doch warum nicht die Erkenntnis, dass zwei Gerüsttests erhalten bleiben sollen, dazu nutzen, die Modularität zu erhöhen und die Regressionsfreiheit zu verbessern? Mehr Module bedeutet mehr Oberfläche zur Überprüfung, ob Code nach Veränderung weiterhin korrekt ist.

Mit der Regressionsfreiheit im Blick könnte das Review-Ergebnis z.B. so aussehen:

public class Aggregator {
    public int Sum(string filename) {
        var file = new FileProvider();

        var numbers = file.Load(filename);
        return Calculate_sum(numbers);
    }

    private int Calculate_sum(IEnumerable<int> numbers) {
        var sum = 0;
        foreach (var n in numbers)
            sum += n;
        return sum;
    }
}

internal class FileProvider {
    public int[] Load(string filename) {
        var lines = System.IO.File.ReadAllLines(filename);
        var numbers = new List<int>();
        for (var i = 0; i < lines.Length; i++)
            if (!string.IsNullOrWhiteSpace(lines[i]))
                numbers.Add(int.Parse(lines[i]));
        return numbers.ToArray();
    }
}

Aggregator{} wäre als Geschäftslogikklasse auf die Summation konzentriert. FileProvider{} würde den Datenzugriff kapseln. Eine saubere Trennung von zwei sehr unterschiedlichen Verantwortlichkeiten, die mittels Tests dokumentiert würde:

[Test()]
public void Akzeptanztest() {
    var sut = new Aggregator();
    var result = sut.Sum("numbers.txt");
    Assert.AreEqual(356, result);
}

[Test]
public void Load_with_empty_lines() {
    var sut = new FileProvider();
    var result = sut.Load("numbers_with_empty_lines.txt");
    Assert.AreEqual(new[] { 1, 2, 3 }, result);
}

[Test]
public void Load_with_whitespace() {
    var sut = new FileProvider();
    var result = sut.Load("numbers_with_whitespace.txt");
    Assert.AreEqual(new[] { 1 }, result);
}

Der Akzeptanztest könnte wieder so einfach sein, wie zunächst mit dem Kunden verabredet, der lediglich diesen Inhalt für numbers.txt vorgesehen hatte:

1
22
333

Der Akzeptanztest wäre fokussierter auf den happy day, auf das Wesentliche, auf die Integration von Teilfunktionalitäten. Sonderfälle würden an der Schnittstelle des Teiles geprüft. Die ist nicht öffentlich dem Kunden bzw. Konsumenten der beauftragten Funktionalität gegenüber; das ist nur die Klasse Aggregate{} mit der Methode Sum(). Aber es wäre immer noch eine Schnittstelle gegenüber Aggregate{} im Inneren des beide umfassenden Modules, z.B. der Bibliothek reporting.dll.

Ausgehend von der Überprüfung der Korrektheit von einzelnen Funktionen trägt die Regressionsfreiheit also dazu bei, Module höherer Ebenen herauszukristallisieren: zunächst Klassen, dann Bibliotheken.

Zusammenfassung

Die Römer haben gesagt „Pacta sunt servanda“ – Verträge sind einzuhalten. Das gilt auch für Software. Wo Verträge mittels öffentlicher Module formuliert werden, müssen sie eingehalten werden. Ob das geschieht, überprüfen automatisierte Tests. Und das nicht nur einmal, wenn Veränderungen erstmalig umgesetzt und ausgeliefert werden, sondern immerdar.

Konsequentes Testen aller Verträge sorgt für (weitgehende) Regressionsfreiheit. Wo Verträge ohne Tests sind, sollten die daher nachgerüstet werden. Wo Verträge neu ausgehandelt werden, sollten sie testgetrieben (test-first) implementiert werden.

Wer diese Regel versteht, kommt um einen diesbezüglichen Review jedoch nicht herum. Review muss weiterhin sein, auch wenn alle Entwickler besten Willens sind, so bei der Umsetzung von Anforderungen vorzugehen. In den Wirren des Tagesgeschäftes kommt selbst der beste Vorsatz einfach zu schnell mal unter die Räder. Außerdem wird bei der Codierung, wenn das Verhalten im Vordergrund steht, nicht jede Chance für einen Kontrakt, d.h. für Entkopplung erkannt. Im Review ist der Modus jedoch grundsätzlich anders, so dass solche Versäumnisse ausgebügelt werden können.

Maturität + Regressionsfreiheit: Werden beide Aspekte systematisch im Review betrachtet, kann die Korrektheit nicht anders, als zu steigen. Die Zahl der Nachbesserungen sinkt. Die Kapazität für Neuerungen wächst. Von der Zufriedenheit beim Kunden ganz zu schweigen.

PS: Der Review bleibt natürlich eine Überprüfung. Wenn Tests erst geschrieben werden, weil der Review ihr Fehlen entdeckt hat, wenn Modularität erst nach dem Review in den Code hineinrefaktorisiert wird, dann läuft die Softwareentwicklung immer noch falsch.

Der Review soll eigentlich nur feststellen, dass während der Codierung alle Tests schon test-first realisiert wurden:

  • Alle Akzeptanztests implementiert (Fundamenttests)? Check!
  • Alle öffentlichen Funktionen unter Test? Check!
  • Wurden aus Gerüsttests weitere sinnvolle Module abgeleitet und unter Test gehalten (Pfeilertests)? Check!
  • Verbliebene Gerüsttests gelöscht? Check!

Eigentlich sollte der Review in dieser Weise durch den Quellcode fließen. In der Realität wird er jedoch Lücken finden. Das ist normal, das ist ok – solange es im Rahmen bleibt. Wird der überschritten und der Review artet aus in eine Nachbesserungsorgie, dann sind vorgelagert Maßnahmen zu ergreifen. Dann muss die Herstellung der gewünschten Eigenschaften geübt werden, um im Tagesgeschäft mühelos von jedem Entwickler durchgeführt werden zu können.

Weitere Artikel in dieser Serie:

Veröffentlicht in Allgemein und verschlagwortet mit .