Testpõhine areng: mis see on ja mis see pole.

Testpõhine arendus on viimase paari aasta jooksul populaarseks saanud. Paljud programmeerijad on seda tehnikat proovinud, ebaõnnestunud ja jõudnud järeldusele, et TDD pole seda vaeva väärt.

Mõned programmeerijad arvavad, et teoreetiliselt on see hea tava, kuid TDD reaalseks kasutamiseks pole kunagi piisavalt aega. Ja teised arvavad, et see on põhimõtteliselt aja raiskamine.

Kui tunnete end nii, arvan, et te ei pruugi aru saada, mis TDD tegelikult on. (OK, eelmine lause pidi teie tähelepanu köitma). Kui soovite seda uurida ja lisateavet saada, on TDD kohta olemas väga hea raamat „Test Driven Development: Example: Kent Beck”.

Selles artiklis tutvun testpõhise arenduse põhialustega, käsitledes levinud eksiarvamusi TDD tehnika osas. See artikkel on ka esimene mitmest artiklist, mida ma avaldan, testipõhise arenduse kohta.

Miks kasutada TDD-d?

TDD efektiivsuse kohta on uuringuid, referaate ja arutelusid. Ehkki mõne numbri olemasolu on kindlasti kasulik, ei usu ma, et nad vastavad küsimusele, miks peaksime üldse TDD-d kasutama.

Oletame, et olete veebiarendaja. Olete just lõpetanud väikese funktsiooni. Kas peate selle funktsiooni testimiseks piisavaks brauseriga käsitsi suheldes? Ma arvan, et ei piisa, kui toetuda ainult arendajate käsitsi tehtud testidele. Kahjuks tähendab see, et osa koodist pole piisavalt hea.

Kuid ülaltoodud kaalutlus puudutab testimist, mitte TDD-d ennast. Miks siis TDD? Lühike vastus on ", sest see on lihtsaim viis nii hea kvaliteediga koodi kui ka hea katte saavutamiseks".

Pikem vastus tuleb sellest, mis TDD tegelikult on ... Alustame reeglitest.

Mängureeglid

Onu Bob kirjeldab TDD-d kolme reegliga:

- Teil on keelatud kirjutada ühtegi tootekoodi, välja arvatud juhul, kui selleks on vaja ebaõnnestunud üksuse testi läbimist. - Te ei tohi kirjutada rohkem ühikutesti kui see on piisav ebaõnnestumiseks; ja kompileerimisvead on tõrked. - Teil pole lubatud kirjutada rohkem tootmiskoodi, kui on piisav ühe ebaõnnestunud üksuse testi läbimiseks.

Mulle meeldib ka lühem versioon, mille leidsin siit:

- Ebaõnnestumiseks kirjutage ainult piisavalt ühikutesti. - Rikkuva üksuse testi läbimiseks kirjutage ainult nii palju tootekoodi.

Need reeglid on lihtsad, kuid TDD-le lähenevad inimesed rikuvad sageli ühte või mitut neist. Ma esitan teile väljakutse: kas saate kirjutada väikeprojekti rangelt järgides neid reegleid? Väikese projekti all mõtlen ma midagi tõelist, mitte ainult näidet, mis nõuab näiteks 50 rida koodi.

Need reeglid määratlevad TDD mehaanika, kuid need pole kindlasti kõik, mida peate teadma. Tegelikult kirjeldatakse TDD kasutamise protsessi sageli kui punase / rohelise / refaktori tsüklit. Vaatame, mis see on.

Red Green Refactori tsükkel

Punane faas

Punases faasis peate kirjutama testi käitumise kohta, mida kavatsete rakendada. Jah, ma kirjutasin käitumist . Sõna “test” testpõhises arenduses on eksitav. Me oleksime pidanud seda nimetama ennekõike „käitumuslikuks arenguks“. Jah, ma tean, mõned inimesed väidavad, et BDD erineb TDD-st, kuid ma ei tea, kas olen nõus. Nii et minu lihtsustatud definitsioonis on BDD = TDD.

Siit tuleb üks levinud väärarusaam: „Kõigepealt kirjutan klassi ja meetodi (kuid rakendamist ei toimu), seejärel kirjutan testi, et selle klassi meetodit testida“. See tegelikult nii ei toimi.

Lähme samm tagasi. Miks nõuab TDD esimene reegel testi kirjutamist enne tootekoodi kirjutamist? Kas me oleme TDD-inimeste maniakid?

RGR-tsükli iga etapp tähistab etappi koodi elutsüklis ja seda, kuidas võiksite sellega suhestuda.

Punases faasis käitute nagu nõudlik kasutaja, kes soovib kirjutatavat koodi kasutada võimalikult lihtsal viisil. Peate kirjutama testi, mis kasutab koodijuppi, nagu oleks see juba rakendatud. Unustage rakendamine! Kui selles faasis mõtlete, kuidas kavatsete tootmiskoodi kirjutada, siis teete seda valesti!

Selles etapis keskendute tulevastele kasutajatele puhta liidese kirjutamisele. See on etapp, kus kujundate, kuidas kliendid teie koodi kasutavad.

See esimene reegel on kõige olulisem ja just reegel muudab TDD tavapärasest testimisest erinevaks. Kirjutate testi, et saaksite siis tootmise koodi kirjutada. Koodi testimiseks ei kirjutata testi.

Vaatame ühte näidet.

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

Ülaltoodud kood on näide selle kohta, kuidas test võib JavaScripti abil välja näha, kasutades Jasmine'i testimisraamistikku. Jasmiini ei pea teadma - piisab sellest, kui mõista, et it(...)see on test ja expect(...).toBe(...)on viis panna Jasmiin kontrollima, kas midagi on ootuspärane.

Ülaltoodud testis olen kontrollinud, et funktsioon LeapYear.isLeap(...)tagastaks trueaasta 1996. Võib arvata, et 1996 on maagiline number ja seega halb tava. See ei ole. Testkoodis on maagilised numbrid head, samas kui tootmiskoodis tuleks neid vältida.

Sellel testil on tegelikult mõned tagajärjed:

  • Liigiaasta kalkulaatori nimi on LeapYear
  • isLeap(...)on staatiline meetod LeapYear
  • isLeap(...)võtab argumendina numbri (mitte näiteks massiivi) ja tagastab truevõi false.

See on üks test, kuid sellel on tegelikult palju tagajärgi! Kas meil on vaja meetodit, et öelda, kas aasta on liigaasta, või on vaja meetodit, mis tagastab algusaasta ja lõppkuupäeva vaheliste hüppeaastate loendi? Kas elementide nimi on tähendusrikas? Need on sellised küsimused, mida peate punases faasis testide kirjutamisel meeles pidama.

Selles etapis peate langetama koodi kasutamise otsused. Lähtute sellest, mida te praegu tegelikult vajate, mitte sellele, mida teie arvates võib vaja minna.

Siit tuleb veel üks viga: ärge kirjutage hulga funktsioone / klasse, mida teie arvates võib vaja minna. Keskenduge rakendatavale funktsioonile ja sellele, mida tegelikult vaja on. Selle funktsiooni jaoks vajaliku kirjutamine on üle kavandatud.

Aga abstraktsioon? Näen seda hiljem, refaktori faasis.

Roheline faas

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.

Kui räägite oma rakenduse testimisest, siis on hea mõte paluda teistel inimestel testida, mida teie meeskond tegi. Kui räägite tootmiskoodi kirjutamisest, siis on see vale lähenemine.

Mis järgmiseks?

See artikkel rääkis TDD filosoofiast ja levinud väärarusaamadest. Kavatsen kirjutada TDD-st muid artikleid, kus näete palju koodi ja vähem sõnu. Kui olete huvitatud Tetrise arendamisest TDD abil, olge kursis!