Testa virzīta attīstība: kas tas ir un kas tas nav.

Testu virzīta izstrāde pēdējos gados ir kļuvusi populāra. Daudzi programmētāji ir izmēģinājuši šo tehniku, izgāzušies un secinājuši, ka TDD nav vērts to piepūles dēļ.

Daži programmētāji domā, ka teorētiski tā ir laba prakse, taču nekad nav pietiekami daudz laika, lai patiešām izmantotu TDD. Un citi domā, ka būtībā tā ir laika tērēšana.

Ja jūs jūtaties šādi, es domāju, ka jūs, iespējams, nesaprotat, kas patiesībā ir TDD. (Labi, iepriekšējais teikums bija piesaistīt jūsu uzmanību). Ir ļoti laba grāmata par TDD, Test Driven Development: With Example, autors: Kents Beks, ja vēlaties to pārbaudīt un uzzināt vairāk.

Šajā rakstā es apskatīšu testu virzītas attīstības pamatus, pievēršoties izplatītiem nepareiziem uzskatiem par TDD tehniku. Šis raksts ir arī pirmais no vairākiem rakstiem, kurus es publicēšu, par testu virzītu attīstību.

Kāpēc izmantot TDD?

Ir pētījumi, dokumenti un diskusijas par TDD efektivitāti. Lai gan noteikti ir noderīgi, ja ir daži skaitļi, es nedomāju, ka viņi atbild uz jautājumu, kāpēc mums vispār vajadzētu izmantot TDD.

Sakiet, ka esat tīmekļa izstrādātājs. Jūs tikko esat pabeidzis nelielu funkciju. Vai jūs uzskatāt, ka ir pietiekami pārbaudīt šo funkciju, tikai manuāli mijiedarbojoties ar pārlūku? Es nedomāju, ka ir pietiekami paļauties tikai uz izstrādātāju manuāli veiktajiem testiem. Diemžēl tas nozīmē, ka daļa koda nav pietiekami laba.

Bet iepriekš minētais apsvērums attiecas uz testēšanu, nevis pašu TDD. Kāpēc tad TDD? Īsā atbilde ir “tāpēc, ka tas ir vienkāršākais veids, kā sasniegt gan labas kvalitātes kodu, gan labu testa pārklājumu”.

Garāka atbilde nāk no tā, kas patiesībā ir TDD ... Sāksim ar noteikumiem.

Spēles noteikumi

Tēvocis Bobs raksturo TDD ar trim noteikumiem:

- Jums nav atļauts rakstīt nevienu ražošanas kodu, ja vien tas nenozīmē, ka vienības testa izturēšana neizdodas. - Jums nav atļauts rakstīt vairāk par vienības testu, nekā pietiek, lai izgāztos; un sastādīšanas kļūmes ir kļūmes. - Jums nav atļauts rakstīt vairāk ražošanas koda, nekā tas ir pietiekams, lai nokārtotu vienu neizdevušos vienības testu.

Man patīk arī īsāka versija, kuru atradu šeit:

- Lai neizdotos, uzrakstiet tikai tik daudz vienības testa. - Uzrakstiet tikai tik daudz ražošanas koda, lai neizpildītā vienības pārbaude būtu veiksmīga.

Šie noteikumi ir vienkārši, taču cilvēki, kas tuvojas TDD, bieži pārkāpj vienu vai vairākus no tiem. Es izaicinu jūs: vai varat uzrakstīt nelielu projektu, stingri ievērojot šos noteikumus? Ar nelielu projektu es domāju kaut ko reālu, ne tikai piemēru, kuram nepieciešamas, piemēram, 50 koda rindas.

Šie noteikumi nosaka TDD mehāniku, taču tie noteikti nav viss, kas jums jāzina. Faktiski TDD izmantošanas procesu bieži raksturo kā sarkana / zaļa / reflektora ciklu. Apskatīsim, kas tas ir.

Red Green Refactor cikls

Sarkanā fāze

Sarkanajā fāzē jums jāuzraksta tests par uzvedību, kuru grasāties īstenot. Jā, es rakstīju uzvedību . Vārds “tests” testu virzītā izstrādē ir maldinošs. Mums to vispirms vajadzēja saukt par “Uz uzvedību balstītu attīstību”. Jā, es zinu, daži cilvēki apgalvo, ka BDD atšķiras no TDD, bet es nezinu, vai es tam piekrītu. Tātad manā vienkāršotajā definīcijā BDD = TDD.

Šeit nāk viens izplatīts nepareizs uzskats: “Vispirms es uzrakstu klasi un metodi (bet ne ieviešanu), tad es uzrakstu testu, lai pārbaudītu šo klases metodi”. Tas faktiski nedarbojas šādā veidā.

Sāksim soli atpakaļ. Kāpēc TDD pirmais noteikums prasa, lai jūs uzrakstītu testu, pirms rakstāt jebkuru produkcijas kodu? Vai mēs esam TDD cilvēku maniaki?

Katrs RGR cikla posms atspoguļo fāzi koda dzīves ciklā un to, kā jūs varētu ar to saistīties.

Sarkanajā fāzē jūs rīkojaties tā, it kā jūs būtu prasīgs lietotājs, kurš vēlas pēc iespējas vienkāršāk izmantot kodu, kas drīz tiks rakstīts. Jums ir jāraksta tests, kurā tiek izmantots koda fragments, it kā tas jau būtu ieviests. Aizmirstiet par ieviešanu! Ja šajā posmā jūs domājat par to, kā jūs rakstīsit ražošanas kodu, jūs to darāt nepareizi!

Šajā posmā jūs koncentrējaties uz tīra interfeisa izveidi nākamajiem lietotājiem. Šajā posmā jūs plānojat, kā klienti izmantos jūsu kodu.

Šis pirmais noteikums ir vissvarīgākais, un tas ir noteikums, kas padara TDD atšķirīgu no parastās pārbaudes. Jūs uzrakstāt testu, lai pēc tam varētu uzrakstīt ražošanas kodu. Jūs nerakstāt testu, lai pārbaudītu kodu.

Apskatīsim piemēru.

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

Iepriekš redzamais kods ir piemērs tam, kā tests varētu izskatīties JavaScript valodā, izmantojot Jasmine testēšanas sistēmu. Jums nav jāzina Jasmīns - pietiek ar to, lai saprastu, ka it(...)tas ir pārbaudījums, un expect(...).toBe(...)tas ir veids, kā likt Jasmīnai pārbaudīt, vai kaut kas ir tā, kā paredzēts.

Iepriekšminētajā pārbaudē esmu pārbaudījis, vai funkcija LeapYear.isLeap(...)atgriežas true1996. gadā. Jūs domājat, ka 1996. gads ir burvju skaitlis un tādējādi slikta prakse. Tas nav. Pārbaudes kodā maģiskie skaitļi ir labi, savukārt ražošanas kodā no tiem vajadzētu izvairīties.

Šim testam faktiski ir dažas sekas:

  • Lēciena gada kalkulatora nosaukums ir LeapYear
  • isLeap(...)ir statiska metode LeapYear
  • isLeap(...)ņem skaitli (nevis masīvu, piemēram) kā argumentu un atgriež truevai false.

Tas ir viens tests, bet tam faktiski ir daudz seku! Vai mums ir nepieciešama metode, lai noteiktu, vai gads ir lēciena gads, vai mums ir nepieciešama metode, kas atgriež lēcienu sarakstu starp sākuma un beigu datumu? Vai elementu nosaukums ir nozīmīgs? Šie ir jautājumi, kas jums jāpatur prātā, rakstot testus sarkanajā fāzē.

Šajā posmā jums jāpieņem lēmumi par koda izmantošanu. Jūs to pamatojat ar to, kas jums patiešām šobrīd ir vajadzīgs, nevis uz to, kas, jūsuprāt, varētu būt vajadzīgs.

Šeit nāk vēl viena kļūda: nerakstiet virkni funkciju / klases, kuras, jūsuprāt, jums varētu būt nepieciešamas. Koncentrējieties uz funkciju, kuru īstenojat, un uz to, kas patiešām ir vajadzīgs. Rakstīt kaut ko, kas šai funkcijai nav vajadzīgs, ir pārmērīga inženierija.

Kā ar abstrakciju? To redzēšu vēlāk, refaktora fāzē.

Zaļā fāze

Parasti tas ir vienkāršākais posms, jo šajā posmā jūs rakstāt (ražošanas) kodu. Ja esat programmētājs, jūs to darāt visu laiku.

Šeit nāk vēl viena liela kļūda: tā vietā, lai uzrakstītu pietiekami daudz koda, lai nokārtotu sarkano testu, jūs rakstāt visus algoritmus. To darot, jūs, iespējams, domājat par to, kas vislabāk darbojas. Nevar būt!

Šajā fāzē jums jārīkojas kā programmētājam, kuram ir viens vienkāršs uzdevums: uzrakstiet vienkāršu risinājumu, kas nodrošina testa nokārtošanu (un testa ziņojumā satraucoši sarkanā krāsa kļūst draudzīga zaļa). Šajā posmā jums ir atļauts pārkāpt paraugpraksi un pat dublēt kodu. Refaktora fāzē tiks noņemta koda dublēšanās.

Bet kāpēc mums ir šāds noteikums? Kāpēc es nevaru uzrakstīt visu kodu, kas jau ir manā prātā? Divu iemeslu dēļ:

  • Vienkāršs uzdevums ir mazāk pakļauts kļūdām, un jūs vēlaties samazināt kļūdu skaitu.
  • Jūs noteikti nevēlaties sajaukt testējamo kodu ar kodu, kas nav. Jūs varat rakstīt kodu, kas netiek testēts (jeb mantojums), bet vissliktākais, ko varat darīt, ir sajaukt pārbaudītu un nepārbaudītu kodu.

Kas par tīru kodu? Kā ar sniegumu? Ko darīt, ja, uzrakstot kodu, es atklāju problēmu? Kā ar šaubām?

Izrāde ir garš stāsts, un tas ir ārpus šī raksta darbības jomas. Teiksim tikai tā, ka veiktspējas regulēšana šajā fāzē lielākoties ir priekšlaicīga optimizācija.

Testa virzīta izstrādes tehnika nodrošina vēl divas lietas: uzdevumu sarakstu un refaktora fāzi.

Refaktora fāze tiek izmantota koda attīrīšanai. Uzdevumu saraksts tiek izmantots, lai pierakstītu nepieciešamās darbības, lai pabeigtu īstenojamo funkciju. Tas satur arī šaubas vai problēmas, kuras atklājat procesa laikā. Iespējamais lēciena gada kalkulatora uzdevumu saraksts varētu būt:

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?

Uzdevumu saraksts ir aktīvs: tas mainās, kamēr jūs kodējat, un ideālā gadījumā objekta ieviešanas beigās tas būs tukšs.

Refaktora fāze

Refaktora fāzē jums ir atļauts mainīt kodu, vienlaikus saglabājot visus testus zaļā krāsā, lai tas kļūtu labāks. Ko nozīmē “labāks”, atkarīgs no jums. Bet ir kaut kas obligāts: jums ir jānoņem koda dublēšanās . Kents Becks savā grāmatā iesaka, ka viss, kas jums jādara, ir koda dublēšanās noņemšana.

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.

Ja jūs runājat par savas lietojumprogrammas testēšanu, jā, ir ieteicams lūgt citiem cilvēkiem pārbaudīt jūsu komandas darbību. Ja jūs runājat par ražošanas koda rakstīšanu, tad tā ir nepareiza pieeja.

Ko tālāk?

Šis raksts bija par TDD filozofiju un izplatītajiem nepareizajiem uzskatiem. Es plānoju rakstīt citus rakstus par TDD, kur jūs redzēsiet daudz kodu un mazāk vārdu. Ja jūs interesē, kā attīstīt Tetris, izmantojot TDD, sekojiet jaunumiem!