Testiohjattu kehitys: mikä se on ja mikä ei ole.

Testikäyttöinen kehitys on tullut suosittu viime vuosina. Monet ohjelmoijat ovat kokeilleet tätä tekniikkaa, epäonnistuneet ja tulleet siihen tulokseen, että TDD ei ole sen vaivan arvoinen.

Jotkut ohjelmoijat ajattelevat, että teoriassa se on hyvä käytäntö, mutta TDD: n käyttämiseen ei ole koskaan tarpeeksi aikaa. Ja toisten mielestä se on pohjimmiltaan ajanhukkaa.

Jos sinusta tuntuu tältä, luulen, ettet ehkä ymmärrä mitä TDD todella on. (OK, edellisen lauseen oli kiinnitettävä huomiosi). TDD: stä on erittäin hyvä kirja, Test Driven Development: Esimerkiksi Kent Beck, jos haluat tarkistaa sen ja oppia lisää.

Tässä artikkelissa käyn läpi testiohjatun kehityksen perusteet käsittelemällä TDD-tekniikkaa koskevia yleisiä väärinkäsityksiä. Tämä artikkeli on myös ensimmäinen useista artikkeleista, jotka aion julkaista, kaikki testikäyttöisestä kehityksestä.

Miksi käyttää TDD: tä?

TDD: n tehokkuudesta on tutkimuksia, papereita ja keskusteluja. Vaikka on ehdottomasti hyödyllistä saada joitain numeroita, en usko, että he vastaavat kysymykseen, miksi meidän pitäisi käyttää TDD: tä ensinnäkin.

Sano, että olet verkkokehittäjä. Olet juuri saanut valmiiksi pienen ominaisuuden. Pitäisitkö tarpeeksi testata tämä ominaisuus vain käsittelemällä selainta manuaalisesti? Mielestäni ei riitä luottaa vain kehittäjien manuaalisesti tekemiin testeihin. Valitettavasti tämä tarkoittaa, että osa koodista ei ole tarpeeksi hyvä.

Mutta yllä oleva huomio koskee testausta, ei itse TDD: tä. Joten miksi TDD? Lyhyt vastaus on ", koska se on yksinkertaisin tapa saavuttaa sekä hyvälaatuinen koodi että hyvä testipeitto."

Pidempi vastaus tulee siitä, mitä TDD todella on ... Aloitetaan säännöistä.

Pelin säännöt

Bob-setä kuvaa TDD: tä kolmella säännöllä:

- Tuotantokoodia ei saa kirjoittaa, ellei sen tarvitse tehdä epäonnistunutta yksikötestin läpäisyä. - Et voi kirjoittaa enempää yksikötestiä kuin riittää epäonnistumiseen. ja kokoamisvirheet ovat epäonnistumisia. - Et voi kirjoittaa enempää tuotantokoodia kuin riittää yhden epäonnistuneen yksikötestin läpäisemiseen.

Pidän myös lyhyemmästä versiosta, jonka löysin täältä:

- Kirjoita vain niin paljon yksikötestiä, että se epäonnistuu. - Kirjoita vain tarpeeksi tuotantokoodia, jotta epäonnistunut yksikön testi läpäisee.

Nämä säännöt ovat yksinkertaisia, mutta TDD: tä lähestyvät ihmiset rikkovat usein yhtä tai useampaa niistä. Haastan sinut: voitko kirjoittaa pienen projektin noudattaen tiukasti näitä sääntöjä? Pienellä projektilla tarkoitan jotain todellista, ei vain esimerkkiä, joka vaatii kuten 50 riviä koodia.

Nämä säännöt määrittelevät TDD: n mekaniikan, mutta ne eivät todellakaan ole kaikki mitä sinun tarvitsee tietää. Itse asiassa TDD: n käyttöprosessia kuvataan usein punaisena / vihreänä / refaktorina. Katsotaanpa, mistä on kyse.

Punainen vihreä Refactor-sykli

Punainen vaihe

Punaisessa vaiheessa sinun on kirjoitettava testi käyttäytymisestä, jonka aiot toteuttaa. Kyllä, kirjoitin käyttäytymistä . Testiohjatun kehityksen sana "testi" on harhaanjohtava. Meidän olisi pitänyt kutsua sitä ensin "käyttäytymiseen perustuvaksi kehitykseksi". Kyllä, tiedän, jotkut väittävät, että BDD eroaa TDD: stä, mutta en tiedä, olenko samaa mieltä. Joten yksinkertaistetussa määritelmässäni BDD = TDD.

Tässä tulee yksi yleinen väärinkäsitys: "Ensin kirjoitan luokan ja menetelmän (mutta ei toteutusta), sitten kirjoitan testin kyseisen luokan menetelmän testaamiseksi". Se ei todellakaan toimi tällä tavalla.

Otetaan askel taaksepäin. Miksi TDD: n ensimmäinen sääntö edellyttää testin kirjoittamista ennen tuotekoodin kirjoittamista? Ovatko me TDD-ihmisten maniakit?

RGR-syklin jokainen vaihe edustaa vaihetta koodin elinkaaressa ja sitä, miten saatat liittyä siihen.

Punaisessa vaiheessa olet kuin vaativa käyttäjä, joka haluaa käyttää kirjoitettavaa koodia yksinkertaisimmalla mahdollisella tavalla. Sinun on kirjoitettava testi, joka käyttää koodikappaletta ikään kuin se olisi jo toteutettu. Unohda toteutus! Jos ajattelet tässä vaiheessa kuinka kirjoitat tuotantokoodin, teet sen väärin!

Tässä vaiheessa keskityt puhtaan käyttöliittymän kirjoittamiseen tuleville käyttäjille. Tässä vaiheessa suunnittelet, kuinka asiakkaasi käyttävät koodiasi.

Tämä ensimmäinen sääntö on tärkein, ja se on sääntö, joka tekee TDD: stä erilaisen kuin säännöllinen testaus. Kirjoitat testin, jotta voit kirjoittaa tuotantokoodin. Et kirjoita testiä koodisi testaamiseksi.

Katsotaanpa esimerkkiä.

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

Yllä oleva koodi on esimerkki siitä, miten testi saattaa näyttää JavaScriptiä käyttäen Jasmine-testauskehystä. Sinun ei tarvitse tietää Jasmiinia - riittää, että ymmärrät, että tämä it(...)on testi ja expect(...).toBe(...)on tapa saada Jasmine tarkistamaan, onko jokin odotettua.

Yllä olevassa testissä olen tarkistanut, että funktio LeapYear.isLeap(...)palaa truevuodelle 1996. Voit ajatella, että 1996 on maaginen numero ja siten huono käytäntö. Se ei ole. Testikoodissa maagiset numerot ovat hyviä, kun taas tuotantokoodeissa niitä tulisi välttää.

Tällä testillä on itse asiassa joitain vaikutuksia:

  • Karkausvuoden laskimen nimi on LeapYear
  • isLeap(...)on staattinen menetelmä LeapYear
  • isLeap(...)vie argumentiksi luvun (eikä esimerkiksi taulukon) ja palauttaa truetai false.

Se on yksi testi, mutta sillä on itse asiassa monia vaikutuksia! Tarvitsemmeko menetelmän kertoa, onko vuosi karkausvuosi vai tarvitsemmeko menetelmän, joka palauttaa luettelon karkausvuosista aloitus- ja lopetuspäivän välillä? Ovatko elementtien nimet mielekkäitä? Nämä ovat sellaisia ​​kysymyksiä, jotka sinun on pidettävä mielessä kirjoittaessasi testejä punaisessa vaiheessa.

Tässä vaiheessa sinun on tehtävä päätökset koodin käytöstä. Perustat tämän siihen, mitä todella tarvitset tällä hetkellä, eikä siihen, mitä luulet tarvitsevasi.

Tässä tulee toinen virhe: älä kirjoita joukko toimintoja / luokkia, joita luulet tarvitsevasi. Keskity omistamaasi ominaisuuteen ja siihen, mitä todella tarvitset. Sellaisen kirjoittaminen, jota ominaisuus ei vaadi, on ylisuunnittelua.

Entä abstraktio? Näen sen myöhemmin refaktorin vaiheessa.

Vihreä vaihe

Tämä on yleensä helpoin vaihe, koska tässä vaiheessa kirjoitat (tuotanto) koodin. Jos olet ohjelmoija, teet sen koko ajan.

Tässä tulee toinen iso virhe: Sen sijaan, että kirjoitat tarpeeksi koodia punaisen testin läpäisemiseksi, kirjoitat kaikki algoritmit. Tätä tehdessäsi olet luultavasti miettinyt, mikä on tehokkain toteutus. Ei onnistu!

Tässä vaiheessa sinun on toimittava kuin ohjelmoija, jolla on yksi yksinkertainen tehtävä: kirjoittaa yksinkertainen ratkaisu, joka tekee testin läpäisyn (ja tekee hälyttävän punaisesta testiraportissa ystävällisen vihreän). Tässä vaiheessa voit rikkoa parhaita käytäntöjä ja jopa kopioida koodia. Koodin kopiointi poistetaan refaktorin vaiheessa.

Mutta miksi meillä on tämä sääntö? Miksi en voi kirjoittaa kaikkia mielessäni jo olevia koodeja? Kahdesta syystä:

  • Yksinkertainen tehtävä on vähemmän altis virheille, ja haluat minimoida virheet.
  • Et todellakaan halua sekoittaa testattavaa koodia koodiin, joka ei ole. Voit kirjoittaa koodia, jota ei testata (alias perintö), mutta pahinta mitä voit tehdä, on sekoittaa testattu ja testaamaton koodi.

Entä puhdas koodi? Entä suorituskyky? Entä jos koodin kirjoittaminen saa minut löytämään ongelman? Entä epäilyt?

Suorituskyky on pitkä tarina, ja se ei kuulu tämän artikkelin piiriin. Sanotaan vain, että suorituskyvyn viritys tässä vaiheessa on suurimmaksi osaksi ennenaikaista optimointia.

Testikäyttöinen kehitystekniikka tarjoaa kaksi muuta asiaa: tehtäväluettelon ja refaktorivaiheen.

Refaktorin vaihetta käytetään koodin puhdistamiseen. Tehtäväluetteloa käytetään kirjoittamaan käyttöön tarvittavan toiminnon suorittamiseen tarvittavat vaiheet. Se sisältää myös epäilyksiä tai ongelmia, jotka löydät prosessin aikana. Karkausvuoden laskimen mahdollinen tehtävälista voi olla:

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?

Tehtäväluettelo on aktiivinen: se muuttuu koodaamisen aikana, ja ihannetapauksessa ominaisuuden toteutuksen lopussa se on tyhjä.

Refaktorin vaihe

Refaktorin vaiheessa voit muuttaa koodia pitämällä kaikki testit vihreinä, jotta siitä tulee parempi. Mitä "parempi" tarkoittaa, on sinun tehtäväsi. Mutta on jotain pakollista: sinun on poistettava päällekkäinen koodi . Kent Becks ehdottaa kirjassaan, että koodin kaksoiskappaleiden poistaminen on kaikki mitä sinun tarvitsee tehdä.

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.

Jos puhut sovelluksesi testaamisesta, kyllä, on hyvä pyytää muita ihmisiä testaamaan, mitä tiimisi teki. Jos puhut tuotantokoodin kirjoittamisesta, se on väärä lähestymistapa.

Mitä seuraavaksi?

Tämä artikkeli käsitteli TDD: n filosofiaa ja yleisiä väärinkäsityksiä. Aion kirjoittaa muita artikkeleita TDD: stä, joissa näet paljon koodia ja vähemmän sanoja. Jos olet kiinnostunut kehittämään Tetristä TDD: n avulla, pysy kuulolla!