Es ist zwei Wochen vor dem geplanten Release. Alle sind euphorisch. Zum ersten Mal ist es gelungen, alle Features vor den Deadlines umzusetzen. Sogar die Intuitiv-Tester sind zufrieden mit der Qualität. Die ersten Alpha-Tests liefen ohne Probleme, und im laufenden Beta-Test kommen keine Beschwerden von den ausgewählten Erstkunden.

Nach dem offiziellen Ende der Corona-Pandemie finden sich endlich alle Projektprotagonisten wieder im Großraumbüro zusammen, um den Projektmeilenstein zu feiern. Nach so langer sozialer Entbehrung fällt jeder in die Arme der Kollegen. Champagner-Laune unter den Leuten, und der Chef lässt ein großzügiges Buffet auffahren, um zu feiern.

Mitten in die Feierstimmung mischt sich eine schlechtgelaunte Person. Der Kollege Mario[1] macht lautstark darauf aufmerksam, dass in mehreren DLLs die Testabdeckung nicht die von der Qualitätsrichtline geforderte 60% Testabdeckung erreicht. Er schließt aus, dass das Produkt so verkauft werden darf, was er auch schon dem Vorstand mitgeteilt hat.

Konkret werden statt 60% Testabdeckung in manchen Komponenten lediglich 47% auf dem Level der Unit-Tests erreicht. Bei einigen der Anwesenden macht sich das Gefühl des Fremdschämens breit, den Verantwortlichen der gerügten Komponenten steht Schamesröte im Gesicht. Niemand will in diesem Moment durch mangelnde Professionalität auffallen. Die Party-Laune ist dahin. Vielen Dank, Mario[1].

Wie konnte es so weit kommen?

Wir haben in den Abteilungen nachgeforscht, wie es zu der niedrigen Testabdeckung kommen konnte, und wir haben lange gegrübelt. Überall stoßen wir auf schlaue und engagierte Leute, die sich voller Ehrgeiz und Leidenschaft im Projekt engagieren. Relativ schnell bemerkten wir, dass sich alle bemängelten Komponenten in denjenigen Code-Bereichen befanden, die gemeinsam mit anderen Produkten genutzt wurden und von der „Commons“-Abteilung betreut werden. Als gemeinschaftlich genutzter Code weitete sich das Problem also auch auf andere Produkte aus – und wurde damit ein akutes Unternehmensrisiko.

Clean Code und SPLE sind die Wurzeln des Übels

Genauer in die Software hineingeschaut, fanden wir dort flächendeckend unüberschaubar viele Methoden mir zum Teil nur einer einzigen Zeile Code, und nirgends hatte eine Methode mehr als sieben Zeilen. Die übereinstimmende Aussage aller Entwickler: „Clean Code sagt, dass Funktionen so klein wie möglich sein sollten“. Das Ergebnis waren schier unendlich viele Klassen, die alle viele Dutzend einzeiliger Methoden enthielten, die sich gegenseitig aufrufen und beinahe alle als private deklariert waren - schließlich wurden sie nur innerhalb der Klasse genutzt.

Auch auffällig war, dass die immer gleichen Hilfsmethodenimplementierungen sich in vielen anderen Klassen als Duplikat wiederfanden. Auf die Duplikate angesprochen bekamen wir übereinstimmend ein etwas betretenes „Das habe ich nicht gewusst, dass es in einer anderen Klasse schon eine identische Implementierung gibt“ zu hören.

Ebenso ist aufgefallen, dass selbst die IDEs lautstark anzeigten, dass viele der Methoden im Code überhaupt nicht mehr referenziert wurden. Ein Entwickler daraufhin: „Wir machen Software Product Line Engineering. Seitdem haben wir keine Ahnung mehr, wer das alles verwendet. Manche Frameworks und Projekte springen da per Reflection rein. Wir hatten schon mal richtig viel Ärger, als wir die laut IDE ungenutzte MethodeonMouseClick() rausgeworfen haben. Also lassen wir lieber alles so, wie es ist.“

Wir fragen den Testingenieur

Auf die niedrige Test-Abdeckung angesprochen, erhielten wir vom Testingenieur die Antwort „Da ist ja fast alles als private deklariert, sogar manche Klassen. Wie soll ich denn da von außen rankommen? Ich muss da echt ausknobeln, wie ich die wenigen öffentlichen Einsprungpunkte bedienen muss, um am Ende die Methode zu erreichen, die mir für die Abdeckung noch fehlt. Obendrein haben manche Methoden auch noch Parameter-Checks, sodass ich da kaum mehr reinkomme. Früher hatte ich geglaubt, dass ich das Innere der Software nicht kennen muss, um Requirements zu testen. Mittlerweile kenne ich die Implementierungen auswendig. Sie erscheinen mir sogar nachts in Albträumen.“

Im Fall der nicht mehr direkt referenzierten Methoden wird der zu bedauernde Testingenieur also niemals Test-Coverage erreichen können, während zugleich der Druck auf ihn steigt, im noch erreichbaren Code umso mehr Abdeckung erreichen zu müssen. Ein Spiel, das er in dieser Projektkultur nicht gewinnen kann.

Der Deal zwischen Entwicklern und Testingenieuren ist unfair

Das Szenario der durch Clean Code begründeten schier unendlichen Anzahl an Klassen und Methoden ist uns nicht neu. In viel zu vielen Fällen haben die Testingenieure Leidensfähigkeit bewiesen, wenn sie trotz Clean Code die Auflagen der Qualitätsrichtlinien und der Prozesse erfüllen konnten. Doch wenn es ganz offensichtlich mehr um Konformität als um Qualität geht, stehen wir konstruktiv zur Seite und geben Hilfsstellungen:

Genauso wie Entwickler aus Beliebigkeit Methoden für Tester unerreichbar machen, QM-Leute ohne Fachkenntnis Grenzwerte festsetzen und Projektleiter die Testingenieure unter Druck setzen, sollten Tester auch ihr gesamtes Repertoire an Mitteln einsetzen dürfen, um unsinnige Metriken zu erfüllen.

Wie kommen wir an den privaten Kram ran?

Das Schüsselwort lautet Reflection: In allen modernen Sprachen sind Hilfestellungen Teil des Sprachkonzepts, die die normalen Sprachmechanismen aushebeln. So ist es beispielsweise möglich, von einem Objekt alle implementierten Methoden herauszufinden, unabhängig von der vermeintlichen Sichtbarkeit:

MeineKlasse meineKlasse = new MeineKlasse();
var methods = meineKlasse.GetType().GetMethods()

Teil dieser Methodeninformation ist sogar die Liste der erwarteten Parameter:

object[] parameters = new object[methods.GetParameters().Length];

Reflection erlaubt obendrein, die Methode in ihrer Instanz aufzurufen, mitsamt ihrer Parameter:

method.Invoke(meineKlasse, parameters);

Dem Testingenieur steht also alles zur Verfügung, um private Methoden direkt erreichen zu können. Aber woher bekommen wir die korrekten Parameter? Vielleicht brauchen wir sie überhaupt nicht, wie wir gleich sehen werden.

Forderungen aus den Safety-Normen spielen uns in die Hände

Safety-Normen wie ISO 61508 oder ISO 26262 fordern von jeder Funktion, dass sie hinsichtlich der Eingabeparameter eine Plausibilisierung implementiert. Diese manchmal als Preconditions-Check bezeichneten Prüfungen finden wir am Anfang jeder Methode. Vor dem Hintergrund, dass Methoden nur wenige Zeilen lang sein dürfen, ist anzunehmen, dass die Implementierung der Preconditions-Check einen relativ hohen Code-Anteil jeder Methode ausmacht. In dieser beispielhaften Funktion sind es immerhin stolze 67% der Lines of Code:

private int CalculateSafetyCriticalSum(long long a, long long b) {
    // check precondition
    if (a + b > LLONG_MAX)
        throw new OutOfRangeException();
    // ok, now we're safe.
    return a + b;
}

Wir machen es uns leicht, und hüpfen also einfach mal in jede Funktion hinein und überzeugen uns davon, dass der Parameter-Check implementiert ist. Im einfachsten Fall mit allen Parametern als null-Value. Unser Code sieht also wie folgt aus:

[TestMethod]
public void TestMeineKlasse()
{
    MeineKlasse meineKlasse = new MeineKlasse();
    foreach (var method in meineKlasse.GetType().GetMethods())
    {
        try
        {
            object[] parameters = new object[method.GetParameters().Length];
            method.Invoke(meineKlasse, parameters);
        }
        catch (Throwable t)
        {
        }
    }
}

Eventuell auftretende Exceptions fangen wir, da wir sie besser von Experten auf Test-Seite hinsichtlich Relevanz bewerten wollen, statt durch fachfremde QM-Leute.

Und das Ergebnis spricht für sich: Bei den meisten kurzen Methoden machen die Precondition-Checks einen hohen prozentualen Anteil der Lines of Code aus, so dass wir durch das willkürliche Einspringen in private Methoden eine erhebliche Steigerung der Coverage erzielen können, selbst wenn die Parameter alle ungültig sind.

Fazit

Niemandem ist geholfen, wenn Clean Code-Hörigkeit zu vielen kleinen privaten Methoden führt, und dabei die Menge der Methoden ins unermessliche steigt. Insbesondere im Kontext des Software Product Engineering ist diese Mischung für Projekte tödlich.

Für den Testentwickler gibt es aber Entwarnung: Dank Reflection werden endlich auch alle privaten Methoden erreichbar, und die Testabdeckung lässt sich über die darin implementierten Precodition-Checks in den Methoden prima in die Höhe treiben.

Der Testingenieur kann aufatmen. Der nächsten Feier steht nichts mehr im Wege.

[1] Name geändert, Name der Redaktion bekannt.