„Test Driven Development“: kas tai yra ir kas ne.

Per pastaruosius kelerius metus bandomoji programa tapo populiari. Daugelis programuotojų išbandė šią techniką, žlugo ir padarė išvadą, kad TDD neverta reikalingų pastangų.

Kai kurie programuotojai mano, kad teoriškai tai yra gera praktika, tačiau niekada nėra pakankamai laiko tikrai naudoti TDD. Kiti mano, kad iš esmės tai yra laiko gaišimas.

Jei taip jausitės, manau, kad galbūt nesuprasite, kas iš tikrųjų yra TDD. (Gerai, ankstesnis sakinys turėjo atkreipti jūsų dėmesį). Yra labai gera knyga apie TDD, Test Driven Development: by Example, autorius Kentas Beckas, jei norite ją patikrinti ir sužinoti daugiau.

Šiame straipsnyje aš apžvelgsiu testuojamo vystymo pagrindus, spręsdamas įprastus klaidingus supratimus apie TDD techniką. Šis straipsnis taip pat yra pirmasis iš daugelio straipsnių, kuriuos paskelbsiu apie testuojamą plėtrą.

Kodėl naudoti TDD?

Yra tyrimų, darbų ir diskusijų apie TDD veiksmingumą. Nors tikrai naudinga turėti keletą skaičių, nemanau, kad jie atsako į klausimą, kodėl pirmiausia turėtume naudoti TDD.

Tarkime, kad esate interneto kūrėjas. Jūs ką tik baigėte mažą funkciją. Ar manote, kad pakanka išbandyti šią funkciją tiesiog rankiniu būdu sąveikaujant su naršykle? Manau, kad nepakanka pasikliauti vien kūrėjų rankiniu būdu atliekamais bandymais. Deja, tai reiškia, kad dalis kodo nėra pakankamai gera.

Bet aukščiau svarstymas yra susijęs su testavimu, o ne pačiu TDD. Taigi kodėl TDD? Trumpas atsakymas yra „nes tai yra paprasčiausias būdas pasiekti ir geros kokybės kodą, ir gerą testo aprėptį“.

Ilgesnį atsakymą pateikia tai, kas iš tikrųjų yra TDD ... Pradėkime nuo taisyklių.

Žaidimo taisyklės

Dėdė Bobas apibūdina TDD pagal tris taisykles:

- Jums neleidžiama rašyti jokio gamybos kodo, nebent tai reiškia, kad nepavyksta atlikti vieneto bandymo. - Jums negalima rašyti daugiau vieneto bandymo, nei pakanka, kad nesėkmė įvyktų; ir kompiliavimo gedimai yra gedimai. - Jums neleidžiama rašyti daugiau gamybos kodo, nei pakanka, kad būtų išlaikytas vienas nepavykęs vieneto testas.

Man taip pat patinka trumpesnė versija, kurią radau čia:

- Parašykite tik tiek, kad nepavyktų atlikti vieneto bandymo. - Parašykite tik tiek gamybos kodo, kad nepavykęs vieneto bandymas būtų išlaikytas.

Šios taisyklės yra paprastos, tačiau žmonės, artėjantys TDD, dažnai pažeidžia vieną ar daugiau jų. Aš jums metu iššūkį: ar galite parašyti nedidelį projektą laikydamiesi griežtai šių taisyklių? Turėdamas omenyje nedidelį projektą, turiu omenyje ką nors tikro, o ne tik pavyzdį, kuriam reikia 50 kodo eilučių.

Tos taisyklės apibrėžia TDD mechaniką, tačiau jos tikrai nėra viskas, ką jums reikia žinoti. Tiesą sakant, TDD naudojimo procesas dažnai apibūdinamas kaip „Raudonos / Žalios“ / „Refaktoriaus“ ciklas. Pažiūrėkime, apie ką kalbama.

„Red Green Refactor“ ciklas

Raudona fazė

Raudonojoje fazėje turite parašyti elgesio, kurį ketinate įgyvendinti, testą. Taip, aš parašiau elgesį . „Test Driven Development“ žodis „testas“ yra klaidinantis. Pirmiausia turėtume tai pavadinti „elgesiu paremta plėtra“. Taip, aš žinau, kai kurie žmonės teigia, kad BDD skiriasi nuo TDD, bet aš nežinau, ar sutinku. Taigi mano supaprastintame apibrėžime BDD = TDD.

Čia pateikiama viena klaidinga nuomonė: „Pirmiausia aš parašau klasę ir metodą (bet neįgyvendinu), tada parašau testą, kad išbandyčiau tą klasės metodą“. Tai iš tikrųjų neveikia tokiu būdu.

Žengkime žingsnį atgal. Kodėl pagal pirmąją TDD taisyklę reikia parašyti testą prieš rašant bet kokį gamybos kodą? Ar mes esame TDD žmonių maniakai?

Kiekvienas RGR ciklo etapas reiškia kodo gyvavimo ciklo etapą ir tai, kaip jūs galite su juo susieti.

Raudonojoje fazėje elgiatės taip, lyg būtumėte reiklus vartotojas, norintis kuo paprasčiau naudoti kodą, kuris bus parašytas. Turite parašyti testą, kuriame naudojamas kodo fragmentas, tarsi jis jau būtų įgyvendintas. Pamiršk apie įgyvendinimą! Jei šiame etape galvojate, kaip rašysite gamybos kodą, tai darote neteisingai!

Šiame etape daugiausia dėmesio skiriate švarios sąsajos rašymui būsimiems vartotojams. Tai etapas, kuriame kuriate, kaip klientai naudos jūsų kodą.

Ši pirmoji taisyklė yra pati svarbiausia, ir būtent ši taisyklė skiria TDD nuo įprasto testavimo. Jūs rašote testą, kad galėtumėte tada parašyti gamybos kodą. Jūs nerašote testo, kad patikrintumėte savo kodą.

Pažvelkime į pavyzdį.

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

Aukščiau pateiktas kodas yra pavyzdys, kaip bandymas gali atrodyti „JavaScript“, naudojant „Jasmine“ testavimo sistemą. Jums nereikia žinoti Jasmine - pakanka suprasti, kad it(...)tai yra testas ir expect(...).toBe(...)yra būdas priversti Jasmine patikrinti, ar kažkas yra taip, kaip tikėtasi.

Pirmiau pateiktame teste patikrinau, ar funkcija LeapYear.isLeap(...)grįžta trueį 1996 metus. Galite pagalvoti, kad 1996 yra stebuklingas skaičius ir todėl yra bloga praktika. Tai nėra. Testo kode stebuklingi skaičiai yra geri, o gamybos koduose jų reikėtų vengti.

Šis testas iš tikrųjų turi tam tikrų pasekmių:

  • Keliamųjų metų skaičiuoklės pavadinimas yra LeapYear
  • isLeap(...)yra statinis metodas LeapYear
  • isLeap(...)kaip argumentą ima skaičių (o ne masyvą) ir grąžina truearba false.

Tai yra vienas testas, tačiau jis iš tikrųjų turi daugybę pasekmių! Ar mums reikia metodo, leidžiančio nustatyti, ar metai yra keliamieji metai, ar mums reikalingas metodas, kuris pateikia kelerių metų sąrašą tarp pradžios ir pabaigos datos? Ar elementų pavadinimas yra prasmingas? Tai yra tokie klausimai, kuriuos turite atsiminti rašydami testus raudonojoje fazėje.

Šiame etape turite priimti sprendimus, kaip kodas bus naudojamas. Tai grindžiate tuo, ko jums šiuo metu tikrai reikia, o ne tuo, ko, jūsų manymu, gali prireikti.

Čia įvyko dar viena klaida: nerašykite krūvos funkcijų / klasių, kurių, jūsų manymu, jums gali prireikti. Sutelkite dėmesį į funkciją, kurią įgyvendinate, ir į tai, ko iš tikrųjų reikia. Parašyti tai, ko funkcijai nereikia, yra per didelė inžinerija.

O abstrakcija? Pamatysime tai vėliau, refaktoriaus etape.

Žalioji fazė

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.

Jei kalbate apie savo programos testavimą, taip patartina paprašyti kitų žmonių išbandyti, ką padarė jūsų komanda. Jei kalbate apie gamybos kodo rašymą, tai neteisingas požiūris.

Kas toliau?

Šis straipsnis buvo apie TDD filosofiją ir paplitusias klaidingas nuostatas. Aš planuoju parašyti kitus straipsnius apie TDD, kur pamatysite daug kodo ir mažiau žodžių. Jei jus domina, kaip sukurti „Tetris“ naudojant TDD, sekite naujienas!