Testdrevet udvikling: hvad det er, og hvad det ikke er.

Testdrevet udvikling er blevet populær i de sidste par år. Mange programmører har prøvet denne teknik, mislykkedes og konkluderede, at TDD ikke er den indsats, det kræver.

Nogle programmører mener, at det i teorien er en god praksis, men at der aldrig er tid nok til virkelig at bruge TDD. Og andre mener, at det dybest set er spild af tid.

Hvis du har det sådan, tror jeg, du måske ikke forstår, hvad TDD virkelig er. (OK, den foregående sætning skulle fange din opmærksomhed). Der er en meget god bog om TDD, Test Driven Development: By Example, af Kent Beck, hvis du vil tjekke den ud og lære mere.

I denne artikel vil jeg gennemgå det grundlæggende i Test Driven Development og tage fat på almindelige misforståelser om TDD-teknikken. Denne artikel er også den første af et antal artikler, jeg vil offentliggøre, alt om testdrevet udvikling.

Hvorfor bruge TDD?

Der er undersøgelser, papirer og diskussioner om, hvor effektiv TDD er. Selvom det bestemt er nyttigt at have nogle tal, tror jeg ikke, de besvarer spørgsmålet om, hvorfor vi i første omgang skal bruge TDD.

Sig, at du er en webudvikler. Du er lige færdig med en lille funktion. Anser du det for nok at teste denne funktion bare ved at interagere manuelt med browseren? Jeg synes ikke det er nok at stole bare på test udført af udviklere manuelt. Desværre betyder det, at en del af koden ikke er god nok.

Men ovenstående overvejelse handler om testning, ikke TDD i sig selv. Så hvorfor TDD? Det korte svar er "fordi det er den enkleste måde at opnå både kvalitetskode og god testdækning".

Det længere svar kommer fra, hvad TDD virkelig er ... Lad os starte med reglerne.

Regler for spillet

Onkel Bob beskriver TDD med tre regler:

- Du har ikke tilladelse til at skrive nogen produktionskode, medmindre den skal bestå en svigtende enhedstest. - Du har ikke tilladelse til at skrive mere af en enhedstest, end det er tilstrækkeligt til at mislykkes; og kompileringsfejl er fiaskoer. - Du har ikke tilladelse til at skrive mere produktionskode, end det er tilstrækkeligt til at bestå den ene enhedstest.

Jeg kan også lide en kortere version, som jeg fandt her:

- Skriv kun nok af en enhedstest til at mislykkes. - Skriv kun nok produktionskode til at få den svigtende enhedstest bestået.

Disse regler er enkle, men folk, der nærmer sig TDD, overtræder ofte en eller flere af dem. Jeg udfordrer dig: kan du skrive et lille projekt efter nøje disse regler? Med lille projekt mener jeg noget rigtigt, ikke kun et eksempel, der kræver som 50 linjer kode.

Disse regler definerer TDD's mekanik, men de er bestemt ikke alt hvad du behøver at vide. Faktisk er processen med at bruge TDD ofte beskrevet som en rød / grøn / refaktor cyklus. Lad os se, hvad det handler om.

Red Green Refactor-cyklus

Rød fase

I den røde fase skal du skrive en test om en adfærd, du er ved at implementere. Ja, jeg skrev adfærd . Ordet "test" i testdrevet udvikling er vildledende. Vi skulle i første omgang have kaldt det "Behavioral Driven Development". Ja, jeg ved, nogle mennesker hævder, at BDD er forskellig fra TDD, men jeg ved ikke, om jeg er enig. Så i min forenklede definition er BDD = TDD.

Her kommer en almindelig misforståelse: “Først skriver jeg en klasse og en metode (men ingen implementering), så skriver jeg en test for at teste den klassemetode”. Det fungerer faktisk ikke på denne måde.

Lad os tage et skridt tilbage. Hvorfor kræver den første regel i TDD, at du skriver en test, før du skriver et stykke produktionskode? Er vi TDD-folk galne?

Hver fase af RGR-cyklussen repræsenterer en fase i kodens livscyklus, og hvordan du kan forholde dig til den.

I den røde fase handler du som om du er en krævende bruger, der ønsker at bruge den kode, der er ved at blive skrevet på den mest enkle måde. Du skal skrive en test, der bruger et stykke kode, som om det allerede var implementeret. Glem implementeringen! Hvis du i denne fase tænker på, hvordan du skal skrive produktionskoden, gør du det forkert!

Det er i denne fase, hvor du koncentrerer dig om at skrive en ren grænseflade til fremtidige brugere. Dette er den fase, hvor du designer, hvordan din kode skal bruges af klienter.

Denne første regel er den vigtigste, og det er reglen, der adskiller TDD fra almindelig test. Du skriver en test, så du derefter kan skrive produktionskode. Du skriver ikke en test for at teste din kode.

Lad os se på et eksempel.

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

Koden ovenfor er et eksempel på, hvordan en test kan se ud i JavaScript ved hjælp af Jasmine-testrammen. Du behøver ikke at kende Jasmine - det er nok at forstå, at det it(...)er en test og expect(...).toBe(...)er en måde at få Jasmine til at kontrollere, om noget er som forventet.

I testen ovenfor har jeg kontrolleret, at funktionen LeapYear.isLeap(...)vender tilbage truefor året 1996. Du tror måske, at 1996 er et magisk tal og derfor er en dårlig praksis. Det er ikke. I testkode er magiske tal gode, mens de i produktionskode skal undgås.

Denne test har faktisk nogle implikationer:

  • Navnet på skudårsregnemaskinen er LeapYear
  • isLeap(...)er en statisk metode til LeapYear
  • isLeap(...)tager et tal (og ikke et array f.eks.) som et argument og returnerer trueeller false.

Det er en test, men det har faktisk mange implikationer! Har vi brug for en metode til at fortælle, om et år er et skudår, eller har vi brug for en metode, der returnerer en liste over skudår mellem start- og slutdato? Er navnet på elementerne meningsfulde? Dette er den slags spørgsmål, du skal huske på, når du skriver tests i den røde fase.

I denne fase skal du træffe beslutninger om, hvordan koden skal bruges. Du baserer dette på, hvad du virkelig har brug for i øjeblikket, og ikke på, hvad du mener kan være nødvendigt.

Her kommer en anden fejltagelse: skriv ikke en masse funktioner / klasser, som du tror, ​​du muligvis har brug for. Koncentrer dig om den funktion, du implementerer, og hvad der virkelig er nødvendigt. At skrive noget, som funktionen ikke kræver, er over-engineering.

Hvad med abstraktion? Vil se det senere i refaktorfasen.

Grøn fase

This is usually the easiest phase, because in this phase you write (production) code. If you are a programmer, you do that all the time.

Here comes another big mistake: instead of writing enough code to pass the red test, you write all the algorithms. While doing this, you are probably thinking about what is the most performing implementation. No way!

In this phase, you need to act like a programmer who has one simple task: write a straightforward solution that makes the test pass (and makes the alarming red on the test report becomes a friendly green). In this phase, you are allowed to violate best practices and even duplicate code. Code duplication will be removed in the refactor phase.

But why do we have this rule? Why can’t I write all the code that is already in my mind? For two reasons:

  • A simple task is less prone to errors, and you want to minimize bugs.
  • You definitely don’t want to mix up code which is under testing with code that is not. You can write code that is not under testing (aka legacy), but the worst thing you can do is mixing up tested and untested code.

What about clean code? What about performance? What if writing code makes me discover a problem? What about doubts?

Performance is a long story, and is out of the scope of this article. Let’s just say that performance tuning in this phase is, most of the time, premature optimization.

The test driven development technique provides two others things: a to-do list and the refactor phase.

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.

Hvis du taler om at teste din ansøgning, ja det er en god ide at bede andre om at teste, hvad dit team gjorde. Hvis du taler om at skrive produktionskode, så er det den forkerte tilgang.

Hvad er det næste?

Denne artikel handlede om TDD's filosofi og almindelige misforståelser. Jeg planlægger at skrive andre artikler om TDD, hvor du vil se en masse kode og færre ord. Hvis du er interesseret i, hvordan du udvikler Tetris ved hjælp af TDD, skal du holde dig opdateret!