Domänenwissen und Craftmanship vereint

Im ersten Teil dieses Artikels wurde gezeigt, wie Fachexperten durch Pair-Programming in die Entwicklung integriert werden. Doch ist der Fachexperte noch immer kein Softwareentwickler. So können Domänenwissen und Craftmanship vereinigt und fachlich korrekte und saubere Unit-Tests geschrieben werden.

Welche Tests verstehen Fachbereich und QA?

Der erste Teil dieses Artikels hat sich damit beschäftigt, warum es sinnvoll ist, den Fachexperten (auch den Qualitätssicherer) in die Entwicklung zu involvieren. Konkret heißt das im Pair eine Story test-driven entwickeln. Doch sind Unit-Tests das Mittel der Wahl, wenn es um Tests geht, die ein programmierfremder verstehen soll?

Unit-Tests sind entwicklungsnah – wirklich?

entwicklungsnahEine verbreitete Meinung ist, Unit-Tests seien entwicklungsnah. In der ein oder anderen Interpretation ist diese Aussage durchaus richtig. Nur die ebenso verbreiteten Schlussfolgerungen daraus sind falsch:

  • Unit-Tests würden sehr technisch testen
  • Unit-Tests würden nichts fachliches testen
  • nur Entwickler würden Unit-Tests verstehen
  • für Fachbereich und QA gäbe es Acceptance-Tests

Ebenfalls verbreitet und fast noch kritischer zu betrachten ist das daraus resultierende Verhalten:

  • Entwickler schreiben Unit-Tests, die nur für Entwickleraugen bestimmt sind
  • Fachbereich und QA interessieren sich nicht für Unit-Tests
  • Acceptance-Tests werden reflexartig erstellt, wenn auch aufwändiger und teils redundant zu den bestehenden Unit-Tests

Unit-Tests sollen fachliche Tests sein

fachlichMeine Herangehensweise ist, soviel fachliche Anforderungen wie nur möglich bereits mit Unit-Tests zu „beweisen“. Dies ist ganz im Sinne der Test-Pyramide.

Damit sind Unit-Tests auch nur fachliche Tests. Sie sind zwar nahe an der Entwicklung, weil sie während der Entwicklung erstellt werden und nahe beim zu testenden Code sind – mehr aber auch nicht.

Ein Unit-Test kann ein Stückchen Funktionalität auf fachliche Korrektheit hin prüfen, wenn dieses Stückchen Funktionalität auch ein geschlossenes Stück Fachlichkeit darstellt. Ein Unit-Test kann also nur so fachlich sein, wie sein Produktiv-Code, den er testet.

Das Command-Pattern in Kombination mit Dependency-Injection ist hierfür geeignet, da sich der Test gesamthaft auf das Verhalten und vor allem auf das Ergebnis der zu testenden Komponente (das Command) beziehen kann. Verhalten und Ergebnisse sind im Regelfall fachliche Anforderungen.

Praktische Realisierung in Java

Im Folgenden möchte ich demonstrieren, wie die erste Annäherung von Fachbereich und Entwickler konkret in Code aussieht. Für die Demo wird nur Java und Mockito verwendet.

Die Domäne

Für ein zu entwickelndes Scrum-Tool soll ein Importer realisiert werden. Dieser Importer holt sich die benötigten Ticket-Informationen von einem fremden Issue-Tracking-System (z.B. Jira) und überführt diese in das eigene Modell. Die User-Story dazu sieht wie folgt aus:

Import von Jira-Issues
Als Product-Owner eines agilen Teams möchte ich die in Jira gepflegten User-Stories einmalig importieren können, um die Migration einfach zu halten und schnell mit dem neuen Tool loslegen kann.
AC01: Import aller Issues
Gegeben ist Jira mit 3 Issues und mein System enthält keine Stories.
Wenn ich den Importer ausgeführt habe, dann enthält mein System 3 Issues.
AC02: Import der Issue-Titel
Gegeben ist ein Jira-Issue mit Titel „Ich bin eine User-Story“ und mein System enthält keine Issues.
Wenn ich den Importer ausgeführt habe, dann enthält mein System ein Issue mit diesem Titel.
AC03: Import der geplanten Zielversion jedes Jira-Issues
Gegeben ist ein Jira-Produkt, dass die geplanten Versionen „1.0“, „1.1“ und „1.2“ enthält. Mein System enthält keine Versionen.
Wenn ich den Importer ausgeführt habe, dann enthält mein System die drei Versionen „1.0“, „1.1“ und „1.2“.
AC04: Zielversionen der Issues sind verknüpft
Gegeben ist ein Jira-Issue das für Version „1.0“ geplant ist. Mein System enthält keine Stories.
Wenn ich den Importer ausgeführt habe, dann enthält mein System ein Issue, das für Version „1.0“ geplant ist

Die Aufgabe ist nun, dass Fachexperte und Entwickler zusammen diese Story im Pair realisieren. Ganz nach TDD wird mit dem Test begonnen.

Entwurf No. 1 – the most dirtiest test

Nach einer längeren Phase Codierung steht nun ein Unit-Test, der die Umsetzung von Akzeptanzkriterium 1 (AC01) einfordert:

@Test public void
should_import_3_issues_from_jira_and_store_them_in_repository() {
 
    JSONObject jsonBacklog = new JSONObject();
    JSONArray jsonIssueArray = new JSONArray();
    jsonBacklog.put("issues", jsonIssueArray);
 
    for (int i = 0; i < 3; i++) {
        JSONObject jsonIssue = new JSONObject();
        jsonIssue.put("key", "ASE-00" + (i + 1));
        jsonIssueArray.put(jsonIssue);
    }
 
    JiraSourceRepository jira = mock(JiraSourceRepository.class);
    when(jira.getBacklog()).thenReturn(jsonBacklog);
 
    ModelRepository repository = mock(ModelRepository.class);
 
    JiraImporter importer = new JiraImporter(jira, repository);
    importer.importAndStore();
 
    ArgumentCaptor issueCaptor = ArgumentCaptor
            .forClass(Issue.class);
    verify(repository, atLeast(0)).storeIssue(issueCaptor.capture());
    List storedIssueList = issueCaptor.getAllValues();
 
    assertThat(storedIssueList.size(), is(3));
}

Dies ist der vielleicht am schnellsten codierte Test der funktioniert. Nicht nur erfahrende Softwareentwickler werden ihre Zeit zum Einlesen und Verstehen benötigen.

Einem Fachexperten und programmierfremden Qualitätssicherer wird verborgen bleiben, was hier passiert. Entsprechend führt dies zur Demotivation und schädigt dem Vorhaben, Fachbereich und Entwicklung zusammenzubringen.

Entwurf No. 2 – private API

Das Entwicker-Fachexperte-Pair einigt sich auf ein anderes Vorgehen: Der Test soll möglichst nahe an der natürlichen Sprache sein. Der nächste Entwurf des Tests sieht wie folgt aus:

@Test public void
should_import_all_3_issues_from_jira_and_store_them_in_repository() {
 
    addJiraIssue("ASE-001");
    addJiraIssue("ASE-002");
    addJiraIssue("ASE-003");
 
    importer.importAndStore();
 
    assertThat(storedIssueList.size(), is(3));
}
 
@Test public void
should_import_title_of_issue() {
 
    addJiraIssue("ASE-001", "Ich bin eine User-Story");
    importer.importAndStore();
 
    assertThat(storedIssueList.get(0).getTitle(),
            is("Ich bin eine User-Story"));
}

Der Entwickler konzentriert sich bei diesem Test darauf, eine möglichst imperative Sprache zu wählen – so wie er es von einer Programmiersprache wie Java gewohnt ist.

Definiert wird die Sprache als private API – alle für die Infrastruktur des Tests notwendigen Methoden sind private Methoden der Test-Klasse:

[...]
 
private JSONObject
addJiraIssue(String withKey) {
    JSONObject jsonIssue = new JSONObject();
    jsonIssue.put("key", withKey);
    jsonIssueArray.put(jsonIssue);
 
    return jsonIssue;
}
 
private void
addJiraIssue(String withKey, String withSummary) {
    JSONObject jsonIssue = addJiraIssue(withKey);
    jsonIssue.put("summary", withSummary);
}
 
[...]

Der Code für die Testinfrastruktur muss dem Fachexperten auch nicht verständlich sein. Die Pflege obliegt dem Handwerk des Entwicklers. Doch genau Pflege ist an dieser Stelle das entscheidende Wort.

pflegeWerden in Zukunft weitere User-Stories um den Importer umgesetzt, wird ähnlicher Infrastruktur-Code immer wieder geschrieben, von verschiedenen Entwicklern (anders) umgesetzt. Es scheint, Infrastruktur-Code sei das persönliche Geheimnis eines jeden Tests. Diese Tatsache erschwert Refactoring der Test-Suites.

Um den Pflegeaufwand zu reduzieren tut man sich und seinem Team also einen großen Gefallen, den testeigenen Infrastruktur-Code auf ein Minimum zu reduzieren und auf Wiederverwendbarkeit zu setzen.

Entwurf No. 3 – shared API

Aus technischer Motivation heraus entscheidet sich der Entwickler, das private API in eine eigene Klasse auszulagern, damit es für andere gleichartige Tests wiederverwendet werden kann.

Dem Fachexperten ist die Befehlsform (addJiraIssue(), assertThat()) noch immer zu weit von den in Prosa formulierten Akzeptanzkriterien entfernt. Er möchte nicht scripten (tu‘ dies, dann das, …) sondern definieren. Der Test sieht dann wie folgt aus:

@Test public void
should_import_all_3_issues_from_jira_and_store_them_in_repository() {
 
    given().issue();
    given().issue();
    given().issue();
 
    whenIssuesImportedAndStored();
 
    then().storedIssues().counts(3);
}
 
@Test public void
should_import_title_of_issue() {
 
    given().issue()
        .withSummary("Ich bin eine User-Story");
 
    whenIssuesImportedAndStored();
 
    then().storedIssue()
        .summary(is("Ich bin eine User-Story"));
}
 
@Test public void
should_associate_issue_with_assigned_targetVersion() {
 
    given().product("ASE")
        .hasVersion("1.0");
    given().issue()
        .withSummary("Ich komme in Version 1.0")
        .plannedFor("1.0");
 
    whenIssuesImportedAndStored();
 
    then().storedIssue()
        .targetVersion().name(is("1.0"));
}

Die Definition des Ausgangszustandes (given) wird durch einen Test-Data-Builder bewerkstelligt. Realisiert als Fluent-API erlaubt er das Schreiben von nahezu sprechenden Sätzen.

Die Verifikation (then) ist hier ebenfalls als Fluent-API realisiert. Die eigentliche Aktion (when) ist als einfache private Methode realisiert, ist dieser Teil doch tatsächlich testspezifisch.

Builder und Verifier können in anderen Tests wiederverwendet werden. So sprechen viele Tests die gleiche Sprache. Der Infrastruktur-Code dieser Test-Klasse beschränkt sich auf die Übersetzung der Methoden given(), when() und then():

private JiraBuilder jiraBuilder = new JiraBuilder();
private ModelRepositoryVerifier verifier = new ModelRepositoryVerifier();
 
private JiraBuilder
given() {
    return jiraBuilder;
}
 
private ModelRepositoryVerifier
then() {
    return verifier;
}
 
private void
whenIssuesImportedAndStored() {
    JiraImporter importer = new JiraImporter(jiraBuilder.get(),
            verifier.modelRepository());
    importer.importAndStore();
}

Entwurf No. 4 – shared Language

Nach mehreren Sprints treffen sich Fachexperte und Entwickler wieder zum Pairing. Inzwischen können alle für das eigene System relevanten Issues aus dem fremden Issue-Tracker importiert werden. Eine neue User-Story fordert das Ausführen diverser Aktionen auf den eigenen Issues.

Entwickler und Fachexperte beginnen mit dem ersten Test. Der Test-Data-Builder für die Import-Stories kann leider nicht wiederverwendet werden, da er intern JSON-Objekte baut:

public class JiraIssueBuilder {
 
    private JSONObject jsonIssue = new JSONObject();
    private JSONObject jsonBacklog;
 
    public JiraIssueBuilder(JSONObject jsonBacklog) {
        this.jsonBacklog = jsonBacklog;
        this.jsonBacklog.getJSONArray("issues").put(jsonIssue);
        withSummary("");
    }
 
    public void withSummary(String summaryToSet) {
        jsonIssue.put("summary", summaryToSet);
    }
 
    [...]
}

Es muss also ein neuer Test-Data-Builder geschrieben werden, der ein Modell der eigenen Domäne baut. Nach kurzer Zeit fällt jedoch dem Pair auf, dass die Sprache, das API, beider Builder identisch ist.

Fachexperte und Entwickler sind sich einig: der Builder soll die Sprache vorgeben, das eigentliche Erbauen aber nicht selbst in die Hand nehmen. Dem Entwickler fallen spontan mehrere Möglichkeiten ein – er entscheidet sich für das Strategy-Pattern. Das obige Beispiel sieht dann wie folgt aus:

public class JiraIssueBuilder {
 
    private JiraBuilderStrategy strategy;
 
    public JiraIssueBuilder(JiraBuilderStrategy strategy) {
        this.strategy = strategy;
        strategy.createIssue();
    }
 
    public void withSummary(String summaryToSet) {
        strategy.setIssuesSummary(summaryToSet);
    }
 
    public void plannedFor(String versionToPlanFor) {
        strategy.setIssuesTargetVersion(versionToPlanFor);
    }
}

Alle Domänenmodell-spezifischen Operationen können nun in ihre eigene Strategy ausgelagert werden. Im Laufe der weiteren Entwicklung entstehen unterschiedliche Strategien:

  • JSON
  • XML
  • eigene Fachklassen
  • PresentationObjects Frontend-seitig

Inzwischen ist der Fachexperte so vertraut im Umgang mit der Sprache, dass er das Schreiben der Unit-Tests übernimmt und somit im Pairprogramming nicht nur neben dem Entwickler sitzt.

Fazit

Durch das gemeinsame Entwickeln mit programmierfremden Fachexperten ergibt sich die Chance, ein sauberes und fachlich lesbares API für Unit-Tests zu erstellen. Gemäß der Test-Pyramide soll die Anzahl der Unit-Tests im System überwiegen. Und Unit-Tests werden gerne als die „entwicklungsnahen“ Tests beschrieben. Nur weil sie am nächsten am Code dran sind, heißt es nicht, dass sie nur von Entwicklern zu verstehen sind.

Durch Unit-Tests kann ein großer Teil der Funktionalität eines Systems verifiziert und Integration- und Acceptance-Tests auf ein Minimum reduziert werden. Deshalb sehe ich es als unverzichtbar, qualitativ gut lesbare und fachlich nachvollziehbare Unit-Tests zu schreiben. Und die hier vorgestellte ist eine Möglichkeit von vielen.

Quellenangaben
Lemtal Sergei/shutterstock.com
Nuk2013/shutterstock.com
k r e f/shutterstock.com
sergo iv/shutterstock.com