Test Driven Development: što to jest, a što nije.

Testirani razvoj postao je popularan posljednjih nekoliko godina. Mnogi programeri isprobali su ovu tehniku, nisu uspjeli i zaključili su da TDD nije vrijedan truda koji mu je potreban.

Neki programeri misle da je to u teoriji dobra praksa, ali da nikada nema dovoljno vremena za stvarno korištenje TDD-a. A drugi misle da je to u osnovi gubljenje vremena.

Ako se tako osjećate, mislim da možda nećete razumjeti što je TDD zapravo. (U redu, prethodna rečenica bila je privući vašu pažnju). Postoji vrlo dobra knjiga o TDD-u, Test Driven Development: By Example, Kenta Becka, ako ga želite provjeriti i naučiti više.

U ovom ću članku proći kroz osnove ispitivanja vođenog testom, baveći se uobičajenim zabludama o TDD tehnici. Ovaj je članak ujedno i prvi od niza članaka koje ću objaviti, a sve o razvoju potaknutom testom.

Zašto koristiti TDD?

Postoje studije, radovi i rasprave o tome koliko je TDD učinkovit. Iako je definitivno korisno imati neke brojeve, mislim da oni ne odgovaraju na pitanje zašto bismo uopće trebali koristiti TDD.

Recimo da ste web programer. Upravo ste završili malu značajku. Smatrate li dovoljno za testiranje ove značajke samo ručnom interakcijom s preglednikom? Mislim da nije dovoljno oslanjati se samo na testove koje su programeri radili ručno. Nažalost to znači da dio koda nije dovoljno dobar.

Ali prethodno razmatranje odnosi se na testiranje, a ne na sam TDD. Pa zašto TDD? Kratki je odgovor "jer je to najjednostavniji način postizanja i koda dobre kvalitete i dobre pokrivenosti testom".

Duži odgovor dolazi od onoga što TDD zapravo jest ... Krenimo od pravila.

Pravila igre

Ujak Bob opisuje TDD s tri pravila:

- Ne smijete pisati bilo koji proizvodni kôd, osim ako ne želite proći neuspješno polaganje jediničnog testa. - Ne smijete napisati više jediničnog testa nego što je dovoljno za neuspjeh; i neuspjesi kompilacije su neuspjesi.- Ne smijete napisati više produkcijskog koda nego što je dovoljno za polaganje jednog neuspjelog jediničnog testa.

Sviđa mi se i kraća verzija koju sam ovdje pronašla:

- Napišite samo dovoljno jediničnog testa da ne uspije. - Napišite samo dovoljno produkcijskog koda da neuspješni jedinični test prođe.

Ta su pravila jednostavna, ali ljudi koji pristupaju TDD-u često krše jedno ili više njih. Izazivam vas: možete li napisati mali projekt slijedeći strogo ova pravila? Pod malim projektom mislim na nešto stvarno, a ne samo na primjer koji zahtijeva oko 50 redaka koda.

Ta pravila definiraju mehaniku TDD-a, ali definitivno nisu sve što trebate znati. U stvari, postupak korištenja TDD-a često se opisuje kao ciklus Crveno / Zeleno / Refaktor. Da vidimo o čemu se radi.

Crveno zeleni ciklus Refaktor

Crvena faza

U crvenoj fazi morate napisati test ponašanja koje ćete provesti. Da, napisao sam ponašanje . Riječ "test" u Test Driven Developmentu obmanjuje. Prvo smo ga trebali nazvati „Razvorom upravljanim ponašanjem“. Da, znam, neki tvrde da se BDD razlikuje od TDD-a, ali ne znam slažem li se. Dakle, u mojoj pojednostavljenoj definiciji, BDD = TDD.

Evo jedne uobičajene zablude: „Prvo napišem klasu i metodu (ali bez implementacije), a zatim napišem test kako bih testirao tu metodu klase“. To zapravo ne funkcionira na ovaj način.

Krenimo korak unatrag. Zašto prvo pravilo TDD-a zahtijeva da napišete test prije nego što napišete bilo koji proizvodni kod? Jesmo li mi TDI ljudi manijaci?

Svaka faza RGR ciklusa predstavlja fazu u životnom ciklusu koda i kako se možete odnositi prema njemu.

U crvenoj fazi ponašate se kao da ste zahtjevan korisnik koji želi koristiti kod koji će biti napisan na najjednostavniji mogući način. Morate napisati test koji koristi dio koda kao da je već implementiran. Zaboravite na provedbu! Ako u ovoj fazi razmišljate o tome kako ćete napisati produkcijski kod, griješite!

U ovoj fazi koncentrirate se na pisanje čistog sučelja za buduće korisnike. Ovo je faza u kojoj dizajnirate kako će klijenti koristiti vaš kod.

Ovo prvo pravilo je najvažnije i pravilo je koje TDD razlikuje od redovnog testiranja. Napišete test da biste zatim mogli napisati produkcijski kod. Ne pišete test da biste testirali svoj kôd.

Pogledajmo primjer.

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

Gornji kôd je primjer kako test može izgledati u JavaScript-u, koristeći Jasminov okvir za testiranje. Jasmine ne trebate znati - dovoljno je shvatiti da it(...)je to test i expect(...).toBe(...)način da se Jasmine provjeri je li nešto onako kako se očekuje.

U gore navedenom testu provjerio sam da se funkcija LeapYear.isLeap(...)vraća trueza 1996. godinu. Možda mislite da je 1996. magični broj i da je prema tome loša praksa. Nije. U testnom kodu magični brojevi su dobri, dok ih u proizvodnom kodu treba izbjegavati.

Taj test zapravo ima neke implikacije:

  • Ime kalkulatora prestupne godine je LeapYear
  • isLeap(...)je statična metoda LeapYear
  • isLeap(...)uzima broj (a ne niz, na primjer) kao argument i vraća trueili false.

To je jedan test, ali zapravo ima mnogo implikacija! Treba li nam metoda kojom ćemo utvrditi je li godina prijestupna ili nam treba metoda koja vraća popis prijestupnih godina između datuma početka i završetka? Jesu li nazivi elemenata smisleni? To su vrste pitanja koja morate imati na umu dok pišete testove u crvenoj fazi.

U ovoj fazi morate donijeti odluku o načinu korištenja koda. To temeljite na onome što vam trenutno treba, a ne na onome što mislite da bi moglo biti potrebno.

Evo još jedne pogreške: nemojte pisati gomilu funkcija / klasa za koje mislite da bi vam mogle trebati. Koncentrirajte se na značajku koju implementirate i na ono što je stvarno potrebno. Pisanje nečega što značajka ne zahtijeva pretjerano je inženjerstvo.

Što je sa apstrakcijom? Vidjet ću to kasnije, u fazi refaktora.

Zelena faza

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.

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!