Test-First-Entwicklung mit FitNesse

In den letzten Jahren habe ich in allen Rollen des Testprozesses mit serverseitigem JavaScript, Perl, PHP, Struts, Swing und modellgetriebenen Architekturen gearbeitet. Alle Projekte waren unterschiedlich, aber alle hatten einige Gemeinsamkeiten: Die Fristen liefen zu spät, und die Projekte hatten Schwierigkeiten, das zu erreichen, was der Kunde wirklich brauchte.

Jedes Projekt hatte irgendeine Anforderung, einige waren sehr detailliert, einige nur wenige Seiten lang. Diese Anforderungen durchliefen normalerweise drei Phasen:

  • Sie wurden geschrieben (entweder vom Kunden oder vom Auftragnehmer) und erhielten eine offizielle Genehmigung
  • Die Tester versuchten, mit den Anforderungen zu arbeiten und fanden sie mehr oder weniger unzureichend
  • Das Projekt trat in eine Phase der Abnahmetests ein, und der Kunde erinnerte sich plötzlich an alle möglichen Dinge, die die Software zusätzlich / anders ausführen musste

Die letzte Phase führte zu Änderungen, die zu Terminüberschreitungen führten, die die Entwickler belasteten, was wiederum zu weiteren Fehlern führte. Die Anzahl der Fehler stieg schnell an und die Gesamtqualität des Systems nahm ab. Klingt bekannt?

Betrachten wir, was in den oben beschriebenen Projekten schief gelaufen ist: Der Kunde, der Entwickler und der Tester haben nicht zusammengearbeitet. Sie gaben die Anforderungen weiter, aber jede Rolle hatte unterschiedliche Bedürfnisse. Darüber hinaus entwickelten die Entwickler normalerweise automatisierte Tests, und die Tester versuchten, auch einige Tests zu automatisieren. Normalerweise konnten sie die Tests nicht ausreichend koordinieren, und viele Elemente wurden zweimal getestet, während andere (normalerweise die harten Teile) überhaupt nicht getestet wurden. Und der Kunde sah keine Tests. Dieser Artikel beschreibt eine Möglichkeit, diese Probleme zu lösen, indem Anforderungen mit automatisierten Tests kombiniert werden.

Geben Sie FitNesse ein

FitNesse ist ein Wiki mit einigen zusätzlichen Funktionen zum Auslösen von JUnit-Tests. Wenn diese Tests mit Anforderungen kombiniert werden, dienen sie als konkrete Beispiele, wodurch die Anforderungen noch deutlicher werden. Darüber hinaus sind die Testdaten logisch organisiert. Das Wichtigste bei der Verwendung von FitNesse ist jedoch die Idee dahinter, was bedeutet, dass die Anforderungen (teilweise) als Tests geschrieben werden, wodurch sie testbar und damit ihre Erfüllung überprüfbar werden.

Mit FitNesse könnte der Entwicklungsprozess folgendermaßen aussehen: Der Anforderungsingenieur schreibt die Anforderungen in FitNesse (anstelle von Word). Er versucht, den Kunden so weit wie möglich einzubeziehen, aber das ist normalerweise nicht täglich erreichbar. Der Tester schaut wiederholt auf das Dokument und stellt vom ersten Tag an schwierige Fragen. Da der Tester anders denkt, denkt er nicht: "Was wird die Software tun?" aber "Was könnte schief gehen? Wie kann ich es brechen?" Der Entwickler denkt eher wie der Anforderungsingenieur. er möchte wissen: "Was muss die Software tun?"

Der Tester beginnt früh mit dem Schreiben seiner Tests, wenn die Anforderungen noch nicht einmal erfüllt sind. Und er schreibt sie in die Anforderungen. Die Tests werden nicht nur Teil der Anforderungen, sondern auch des Überprüfungs- / Akzeptanzprozesses für die Anforderungen, der einige wichtige Vorteile hat:

  • Der Kunde kann auch über die Tests nachdenken. Normalerweise ist sie sogar daran beteiligt, sie zu erstellen (Sie werden überrascht sein, wie viel Spaß sie damit haben kann).
  • Die Spezifikation wird viel detaillierter und präziser, da die Tests normalerweise präziser sind als nur Text.
  • Wenn Sie frühzeitig über reale Szenarien nachdenken, Testdaten bereitstellen und Beispiele berechnen, erhalten Sie eine viel klarere Ansicht der Software - wie bei einem Prototyp, nur mit mehr Funktionen.

Schließlich werden die Anforderungen an den Entwickler übergeben. Er hat jetzt eine leichtere Aufgabe, da sich die Spezifikation aufgrund aller enthaltenen Beispiele weniger wahrscheinlich ändert. Schauen wir uns an, wie dieser Prozess die Arbeit eines Entwicklers erleichtert.

Test zuerst implementieren

Normalerweise ist der schwierigste Teil beim Starten der Test-First-Entwicklung, dass niemand so viel Zeit damit verbringen möchte, Tests zu schreiben, nur um dann einen Weg zu finden, sie zum Laufen zu bringen. Mit dem oben beschriebenen Verfahren erhält der Entwickler die Funktionstests im Rahmen seines Vertrags. Seine Aufgaben ändern sich von "Bauen Sie das, was ich will und Sie sind fertig, bis ich Ihre Arbeit untersuche und Änderungen vornehme" zu "Lassen Sie diese Tests funktionieren und Sie sind fertig". Jetzt hat jeder eine bessere Vorstellung davon, was zu tun ist, wann die Arbeit abgeschlossen sein soll und wo das Projekt steht.

Nicht alle diese Tests werden automatisiert und nicht alle werden Komponententests sein. Wir teilen Tests normalerweise in die folgenden Kategorien ein (Details folgen):

  • Datengesteuerte Tests, die als Komponententests implementiert werden müssen. Berechnungen sind das typische Beispiel.
  • Schlüsselwortgesteuerte Tests, die die Anwendungsnutzung automatisieren. Dies sind Systemtests, für die die Anwendung ausgeführt werden muss. Auf Schaltflächen wird geklickt, Daten werden eingegeben und die resultierenden Seiten / Bildschirme werden auf bestimmte Werte überprüft. Das Testteam implementiert diese Tests normalerweise, aber auch einige Entwickler profitieren davon.
  • Manuelle Tests. Diese Tests sind entweder zu teuer für die Automatisierung und die möglichen Fehler nicht schwerwiegend genug, oder sie sind so grundlegend (Startseite nicht angezeigt), dass ihr Bruch sofort entdeckt wird.

Als ich 2004 zum ersten Mal über FitNesse las, lachte ich und sagte, dass es niemals funktionieren würde. Die Idee, meine Tests in ein Wiki zu schreiben, das sie automatisch in Tests verwandelt, schien zu absurd. Es stellte sich heraus, dass ich falsch lag. FitNesse ist wirklich so einfach wie es scheint.

Diese Einfachheit beginnt mit der Installation. Laden Sie einfach die vollständige Distribution von FitNesse herunter und entpacken Sie sie. In der folgenden Diskussion gehe ich davon aus, dass Sie die Distribution nach C: \ fitnesse entpackt haben.

Starten Sie FitNesse, indem Sie das Skript run.bat( run.shunter Linux) in C: \ fitnesse ausführen. Standardmäßig führt FitNesse einen Webserver auf Port 80 aus. Sie können jedoch einen anderen Port angeben, z. B. 81, indem Sie -p 81der ersten Zeile in der Batchdatei hinzufügen . Das ist alles dazu; Sie können jetzt unter // localhost: 81 auf FitNesse zugreifen.

In diesem Artikel verwende ich die Java-Version von FitNesse unter Windows. Die Beispiele können jedoch auch für andere Versionen (Python, .Net) und Plattformen verwendet werden.

Einige Tests

Die Online-Dokumentation von FitNesse enthält einige einfache Beispiele (vergleichbar mit dem berüchtigten Geldbeispiel von JUnit), um Ihnen den Einstieg zu erleichtern. Sie sind in Ordnung, um den Umgang mit FitNesse zu lernen, aber sie sind nicht kompliziert genug, um einige Skeptiker zu überzeugen. Daher werde ich ein reales Beispiel aus einem meiner letzten Projekte verwenden. Ich habe das Problem erheblich vereinfacht, und der Code, der nicht direkt aus dem Projekt stammt, wurde zur Veranschaulichung geschrieben. Dennoch sollte dieses Beispiel ausreichend kompliziert sein, um die Leistungsfähigkeit von FitNesse zu demonstrieren.

Nehmen wir an, wir arbeiten an einem Projekt, das eine komplexe Java-basierte Unternehmenssoftware für eine große Versicherungsgesellschaft implementiert. Das Produkt wird das gesamte Geschäft des Unternehmens abwickeln, einschließlich Kunden- und Vertragsmanagement und Zahlungen. In unserem Beispiel sehen wir uns einen winzigen Teil dieser Anwendung an.

In der Schweiz haben Eltern Anspruch auf ein Kindergeld pro Kind. Sie erhalten diese Zulage nur, wenn bestimmte Umstände erfüllt sind und der Betrag variiert. Das Folgende ist eine vereinfachte Version dieser Anforderung. Wir werden mit "traditionellen" Anforderungen beginnen und diese anschließend auf FitNesse übertragen.

Es gibt mehrere Phasen des Kindergeldes. Der Anspruch beginnt am ersten Tag des Monats, in dem das Kind geboren wird, und endet am letzten Tag des Monats, an dem das Kind die Altersgrenze erreicht, seine Ausbildung beendet oder stirbt.

Ab dem 12. Lebensjahr wird der Anspruch ab dem ersten Tag des Geburtsmonats auf 190 CHF (offizielles Währungssymbol der Schweiz) erhöht.

Vollzeit- und Teilzeitbeschäftigung der Eltern führen zu unterschiedlichen Ansprüchen, wie in Abbildung 1 dargestellt.

Die aktuelle Beschäftigungsquote wird anhand der aktiven Arbeitsverträge berechnet. Der Vertrag muss gültig sein, und wenn ein Enddatum festgelegt ist, muss er sich in der "Aktivierungsperiode" befinden. Abbildung 2 zeigt, wie viel Geld ein Elternteil je nach Alter des Kindes hat.

Die Regelungen für diese Zahlungen werden alle zwei Jahre angepasst.

Bei der ersten Lesung klingt die Spezifikation möglicherweise genau, und ein Entwickler sollte sie problemlos implementieren können. Aber sind wir uns über die Randbedingungen wirklich sicher? Wie würden wir diese Anforderungen testen?

Randbedingungen
Boundary conditions are the situations directly on, above, and beneath the edges of input and output equivalence classes. Experiences show that test cases exploring boundary conditions have a higher payoff than test cases that do not. A typical example is the infamous "one-off" on loops and arrays.

Scenarios can be a great help in finding exceptions and boundary conditions, as they provide a good way to get domain experts to talk about business.

Scenarios

For most projects, the requirements engineer hands the specification to the developer, who studies the requirements, asks some questions, and starts to design/code/test. Afterwards, the developer hands the software to the test team and, after some rework and fixes, passes it on to the customer (who will likely think of some exceptions requiring changes). Moving the text to FitNesse won't change this process; however, adding examples, scenarios, and tests will.

Scenarios are especially helpful for getting the ball rolling during testing. Some examples follow. Answering the question of how much child allowance is to be paid to each will clarify a lot:

  • Maria is a single parent. She has two sons (Bob, 2, and Peter, 15) and works part-time (20 hours per week) as a secretary.
  • Maria loses her job. Later, she works 10 hours per week as a shop assistant and another 5 hours as a babysitter.
  • Paul and Lara have a daughter (Lisa, 17) who is physically challenged and a son (Frank, 18) who is still in university.

Just talking through these scenarios should help the testing process. Executing them manually on the software will almost certainly find some loose ends. Think we can't do that, since we don't have a prototype yet? Why not?

Keyword-driven testing

Keyword-driven testing can be used to simulate a prototype. FitNesse allows us to define keyword-driven tests (see "Totally Data-Driven Automated Testing" for details). Even with no software to (automatically) execute the tests against, keyword-driven tests will help a lot.

Figure 3 shows what a keyword-driven test might look like. The first column represents keywords from FitNesse. The second column represents methods in a Java class (we write those, and they need to follow the restrictions put on method names in Java). The third column represents data entered into the method from the second column. The last row demonstrates what a failed test might look like (passed tests are green). As you can see, it is quite easy to find out what went wrong.

Such tests are easy and even fun to create. Testers without programming skills can create them, and the customer can read them (after a short introduction).

Defining tests this way, right next to the requirement, has some important advantages over the traditional definition of test cases, even without automation:

  • The context is at hand. The test case itself can be written with the least possible amount of work and is still precise.
  • If the requirement changes, there is a strong chance that the test will change as well (not very likely when several tools are used).
  • The test can be executed at once to show what needs to be fixed to make this new/changed requirement work.

To automate the test, a thin layer of software is created, which is delegated to the real test code. These tests are especially useful for automating manual GUI tests. I developed a test framework based on HTTPUnit for automating the testing of Webpages.

Here is the code automatically executed by FitNesse:

package stephanwiesner.javaworld;

import fit.ColumnFixture;

public class ChildAllowanceFixture extends ColumnFixture { public void personButton() { System.out.println("pressing person button"); } public void securityNumber(int number) { System.out.println("entering securityNumber " + number); } public int childAllowance() { System.out.println("calculating child allowance"); return 190; } [...] }

The output of the tests can be examined in FitNesse as well (see Figure 4), which greatly helps with debugging. In contrast to JUnit, where one is discouraged from writing debug messages, I find them absolutely necessary when working with automated Web tests.

When testing a Web-based application, error pages are included in the FitNesse page and displayed, making debugging much easier than working with log files.

Data-driven testing

Während schlüsselwortgesteuertes Testen für die GUI-Automatisierung in Ordnung ist, ist datengesteuertes Testen die erste Wahl für das Testen von Code, der jede Art von Berechnung durchführt. Wenn Sie bereits Unit-Tests geschrieben haben, was ist das nervigste an diesen Tests? Wahrscheinlich denken Sie an Daten. Ihre Tests sind voller Daten, die sich häufig ändern und die Wartung zu einem Albtraum machen. Das Testen verschiedener Kombinationen erfordert unterschiedliche Daten, was Ihre Tests wahrscheinlich zu komplizierten, hässlichen Tieren macht.