Testgetriebene Entwicklung: Was es ist und was nicht.

Testgetriebene Entwicklung ist in den letzten Jahren populär geworden. Viele Programmierer haben diese Technik ausprobiert, sind gescheitert und sind zu dem Schluss gekommen, dass TDD den erforderlichen Aufwand nicht wert ist.

Einige Programmierer halten dies theoretisch für eine gute Praxis, aber es bleibt nie genug Zeit, um TDD wirklich zu nutzen. Und andere denken, dass es im Grunde Zeitverschwendung ist.

Wenn Sie so denken, werden Sie vielleicht nicht verstehen, was TDD wirklich ist. (OK, der vorherige Satz sollte Ihre Aufmerksamkeit erregen). Es gibt ein sehr gutes Buch über TDD, Test Driven Development: By Example, von Kent Beck, wenn Sie es ausprobieren und mehr erfahren möchten.

In diesem Artikel werde ich die Grundlagen der testgetriebenen Entwicklung erläutern und häufige Missverständnisse über die TDD-Technik ansprechen. Dieser Artikel ist auch der erste einer Reihe von Artikeln, die ich veröffentlichen werde, alles über testgetriebene Entwicklung.

Warum TDD verwenden?

Es gibt Studien, Artikel und Diskussionen darüber, wie effektiv TDD ist. Obwohl es auf jeden Fall nützlich ist, einige Zahlen zu haben, denke ich nicht, dass sie die Frage beantworten, warum wir TDD überhaupt verwenden sollten.

Angenommen, Sie sind ein Webentwickler. Sie haben gerade eine kleine Funktion fertiggestellt. Halten Sie es für ausreichend, diese Funktion nur durch manuelle Interaktion mit dem Browser zu testen? Ich denke nicht, dass es ausreicht, sich nur auf Tests zu verlassen, die von Entwicklern manuell durchgeführt wurden. Leider bedeutet dies, dass ein Teil des Codes nicht gut genug ist.

Bei der obigen Überlegung geht es jedoch um das Testen, nicht um TDD selbst. Warum also TDD? Die kurze Antwort lautet: „Weil dies der einfachste Weg ist, um sowohl einen qualitativ hochwertigen Code als auch eine gute Testabdeckung zu erzielen.“

Die längere Antwort kommt von dem, was TDD wirklich ist ... Beginnen wir mit den Regeln.

Spielregeln

Onkel Bob beschreibt TDD mit drei Regeln:

- Sie dürfen keinen Produktionscode schreiben, es sei denn, Sie müssen einen fehlgeschlagenen Komponententest bestehen. - Sie dürfen nicht mehr von einem Komponententest schreiben, als ausreicht, um fehlzuschlagen. und Kompilierungsfehler sind Fehler. - Sie dürfen nicht mehr Produktionscode schreiben, als ausreicht, um den einen fehlgeschlagenen Komponententest zu bestehen.

Ich mag auch eine kürzere Version, die ich hier gefunden habe:

- Schreiben Sie nur genug von einem Unit-Test, um fehlzuschlagen. - Schreiben Sie nur genug Produktionscode, um den fehlgeschlagenen Unit-Test zu bestehen.

Diese Regeln sind einfach, aber Menschen, die sich TDD nähern, verletzen häufig eine oder mehrere von ihnen. Ich fordere Sie heraus: Können Sie ein kleines Projekt schreiben, das genau diesen Regeln folgt ? Mit kleinem Projekt meine ich etwas Reales, nicht nur ein Beispiel, das etwa 50 Codezeilen erfordert.

Diese Regeln definieren die Mechanik von TDD, aber sie sind definitiv nicht alles, was Sie wissen müssen. Tatsächlich wird der Prozess der Verwendung von TDD häufig als Rot / Grün / Refaktor-Zyklus beschrieben. Mal sehen, worum es geht.

Rot-Grün-Refaktor-Zyklus

Rote Phase

In der roten Phase müssen Sie einen Test für ein Verhalten schreiben, das Sie implementieren möchten. Ja, ich habe Verhalten geschrieben . Das Wort "Test" in Test Driven Development ist irreführend. Wir hätten es in erster Linie „Verhaltensorientierte Entwicklung“ nennen sollen. Ja, ich weiß, einige Leute argumentieren, dass BDD anders ist als TDD, aber ich weiß nicht, ob ich damit einverstanden bin. In meiner vereinfachten Definition ist BDD = TDD.

Hier kommt ein häufiges Missverständnis: "Zuerst schreibe ich eine Klasse und eine Methode (aber keine Implementierung), dann schreibe ich einen Test, um diese Klassenmethode zu testen." Das funktioniert eigentlich nicht so.

Machen wir einen Schritt zurück. Warum erfordert die erste TDD-Regel, dass Sie einen Test schreiben, bevor Sie einen Produktionscode schreiben? Sind wir TDD-Leute verrückt?

Jede Phase des RGR-Zyklus repräsentiert eine Phase im Lebenszyklus des Codes und wie Sie sich darauf beziehen könnten.

In der roten Phase verhalten Sie sich wie ein anspruchsvoller Benutzer, der den Code, der geschrieben werden soll, auf einfachste Weise verwenden möchte. Sie müssen einen Test schreiben, der einen Code verwendet, als wäre er bereits implementiert. Vergessen Sie die Implementierung! Wenn Sie in dieser Phase darüber nachdenken, wie Sie den Produktionscode schreiben, machen Sie es falsch!

In dieser Phase konzentrieren Sie sich darauf, eine saubere Oberfläche für zukünftige Benutzer zu schreiben. In dieser Phase legen Sie fest, wie Ihr Code von Clients verwendet wird.

Diese erste Regel ist die wichtigste und unterscheidet TDD von regulären Tests. Sie schreiben einen Test, damit Sie dann Produktionscode schreiben können. Sie schreiben keinen Test, um Ihren Code zu testen.

Schauen wir uns ein Beispiel an.

// LeapYear.spec.jsdescribe('Leap year calculator', () => { it('should consider 1996 as leap', () => { expect(LeapYear.isLeap(1996)).toBe(true); });});

Der obige Code ist ein Beispiel dafür, wie ein Test in JavaScript unter Verwendung des Jasmine-Testframeworks aussehen könnte. Sie müssen Jasmine nicht kennen - es reicht zu verstehen, dass dies it(...)ein Test ist und expect(...).toBe(...)eine Möglichkeit ist, Jasmine zu überprüfen, ob etwas wie erwartet ist.

Im Test oben habe ich überprüft , dass die Funktion LeapYear.isLeap(...)kehrt truefür das Jahr 1996. Sie mögen denken, dass 1996 eine magische Zahl ist und somit eine schlechte Praxis. Es ist nicht. Im Testcode sind magische Zahlen gut, während sie im Produktionscode vermieden werden sollten.

Dieser Test hat tatsächlich einige Auswirkungen:

  • Der Name des Schaltjahrrechners lautet LeapYear
  • isLeap(...)ist eine statische Methode von LeapYear
  • isLeap(...)Nimmt eine Zahl (und beispielsweise kein Array) als Argument und gibt trueoder zurück false.

Es ist ein Test, aber er hat tatsächlich viele Auswirkungen! Benötigen wir eine Methode, um festzustellen, ob ein Jahr ein Schaltjahr ist, oder benötigen wir eine Methode, die eine Liste der Schaltjahre zwischen einem Start- und einem Enddatum zurückgibt? Sind die Namen der Elemente sinnvoll? Dies sind die Fragen, die Sie beim Schreiben von Tests in der roten Phase berücksichtigen müssen.

In dieser Phase müssen Sie entscheiden, wie der Code verwendet wird. Sie stützen sich auf das, was Sie im Moment wirklich brauchen, und nicht auf das, was Sie für nötig halten.

Hier kommt ein weiterer Fehler: Schreiben Sie keine Reihe von Funktionen / Klassen, die Sie möglicherweise benötigen. Konzentrieren Sie sich auf die Funktion, die Sie implementieren, und auf das, was wirklich benötigt wird. Das Schreiben von etwas, das für die Funktion nicht erforderlich ist, ist überentwickelt.

Was ist mit Abstraktion? Werde das später in der Refaktorphase sehen.

Grüne Phase

Dies ist normalerweise die einfachste Phase, da Sie in dieser Phase (Produktions-) Code schreiben. Wenn Sie ein Programmierer sind, tun Sie das die ganze Zeit.

Hier kommt ein weiterer großer Fehler: Anstatt genug Code zu schreiben, um den roten Test zu bestehen, schreiben Sie alle Algorithmen. Dabei denken Sie wahrscheinlich darüber nach, welche Implementierung am leistungsstärksten ist. Auf keinen Fall!

In dieser Phase müssen Sie sich wie ein Programmierer verhalten, der eine einfache Aufgabe hat: Schreiben Sie eine einfache Lösung, die den Test besteht (und das alarmierende Rot im Testbericht wird zu einem freundlichen Grün). In dieser Phase dürfen Sie gegen Best Practices verstoßen und sogar Code duplizieren. Code-Duplikate werden in der Refactor-Phase entfernt.

Aber warum haben wir diese Regel? Warum kann ich nicht den gesamten Code schreiben, der mir bereits in den Sinn kommt? Aus zwei Gründen:

  • Eine einfache Aufgabe ist weniger fehleranfällig und Sie möchten Fehler minimieren.
  • Sie möchten definitiv keinen Code, der getestet wird, mit Code verwechseln, der nicht getestet wird. Sie können Code schreiben, der nicht getestet wird (auch bekannt als Legacy), aber das Schlimmste, was Sie tun können, ist, getesteten und nicht getesteten Code zu verwechseln.

Was ist mit sauberem Code? Was ist mit Leistung? Was ist, wenn ich beim Schreiben von Code ein Problem entdecke? Was ist mit Zweifeln?

Leistung ist eine lange Geschichte und fällt nicht in den Geltungsbereich dieses Artikels. Sagen wir einfach, dass die Leistungsoptimierung in dieser Phase meistens eine vorzeitige Optimierung ist.

Die testgetriebene Entwicklungstechnik bietet zwei weitere Funktionen: eine Aufgabenliste und die Refaktorphase.

The refactor phase is used to clean up the code. The to-do list is used to write down the steps required to complete the feature you are implementing. It also contains doubts or problems you discover during the process. A possible to-do list for the leap year calculator could be:

Feature: Every year that is exactly divisible by four is a leap year, except for years that are exactly divisible by 100, but these centurial years are leap years if they are exactly divisible by 400.
- divisible by 4- but not by 100- years divisible by 400 are leap anyway
What about leap years in Julian calendar? And years before Julian calendar?

The to-do list is live: it changes while you are coding and, ideally, at the end of the feature implementation it will be blank.

Refactor phase

In the refactor phase, you are allowed to change the code, while keeping all tests green, so that it becomes better. What “better” means is up to you. But there is something mandatory: you have to remove code duplication. Kent Becks suggests in his book that removing code duplication is all you need to do.

In this phase you play the part of a picky programmer who wants to fix/refactor the code to bring it to a professional level. In the red phase, you’re showing off your skills to your users. But in the refactor phase, you’re showing off your skills to the programmers who will read your implementation.

Removing code duplication often results in abstraction. A typical example is when you move two pieces of similar code into a helper class that works for both the functions/classes where the code has been removed.

For example the following code:

class Hello { greet() { return new Promise((resolve) => { setTimeout(()=>resolve('Hello'), 100); }); }}class Random { toss() { return new Promise((resolve) => { setTimeout(()=>resolve(Math.random()), 200); }); }}new Hello().greet().then(result => console.log(result));new Random().toss().then(result => console.log(result));

could be refactored into:

class Hello { greet() { return PromiseHelper.timeout(100).then(() => 'hello'); }}class Random { toss() { return PromiseHelper.timeout(200).then(() => Math.random()); }}class PromiseHelper { static timeout(delay) { return new Promise(resolve => setTimeout(resolve, delay)); }}const logResult = result => console.log(result);new Hello().greet().then(logResult);new Random().toss().then(logResult);

As you can see, in order to remove thenew Promise and setTimeout code duplication, I created a PromiseHelper.timeout(delay) method, which serves both Hello and Random classes.

Just keep in mind that you cannot move to another test unless you’ve removed all the code duplication.

Final considerations

In this section I will try to answer to some common questions and misconceptions about Test Drive Development.

  • T.D.D. requires much more time than “normal” programming!

What actually requires a lot of time is learning/mastering TDD as well as understanding how to set up and use a testing environment. When you are familiar with the testing tools and the TDD technique, it actually doesn’t require more time. On the contrary, it helps keep a project as simple as possible and thus saves time.

  • How many test do I have to write?

The minimum amount that lets you write all the production code. The minimum amount, because every test slows down refactoring (when you change production code, you have to fix all the failing tests). On the other hand, refactoring is much simpler and safer on code under tests.

  • With Test Driven Development I don’t need to spend time on analysis and on designing the architecture.

This cannot be more false. If what you are going to implement is not well-designed, at a certain point you will think “Ouch! I didn’t consider…”. And this means that you will have to delete production and test code. It is true that TDD helps with the “Just enough, just in time” recommendation of agile techniques, but it is definitely not a substitution for the analysis/design phase.

  • Should test coverage be 100%?

No. As I said earlier, don’t mix up tested and untested code. But you can avoid using TDD on some parts of a project. For example I don’t test views (although a lot of frameworks make UI testing easy) because they are likely to change often. I also ensure that there is very a little logic inside views.

  • I am able to write code with very a few bugs, I don’t need testing.

You may able to to that, but is the same consideration valid for all your team members? They will eventually modify your code and break it. It would be nice if you wrote tests so that a bug can be spotted immediately and not in production.

  • TDD works well on examples, but in a real application a lot of the code is not testable.

I wrote a whole Tetris (as well as progressive web apps at work) using TDD. If you test first, code is clearly testable. It is more a matter of understanding how to mock dependencies and how to write simple but effective tests.

  • Tests should not be written by the developers who write the code, they should be written by others, possibly QA people.

If you are speaking about testing your application, yes it is a good idea to ask other people to test what your team did. If you are speaking about writing production code, then that’s the wrong approach.

What’s next?

This article was about the philosophy and common misconceptions of TDD. I am planning to write other articles on TDD where you will see a lot of code and fewer words. If you are interested on how to develop Tetris using TDD, stay tuned!