I gang med Test Driven JavaScript Development (TDD)

I dag anerkender mange – om end ikke de fleste – udviklere, de store fordele man kan få gennem unit-testing. Af samme grund har Test Driven Development (TDD) også vundet stort indpas hos mange virksomheder, især i forbindelse med brugen af de agile metoder. Alligevel virker det til, at dette aldrig rigtigt har slået igennem for hjemmesider, selv om de fleste af de moderne værktøjer opmuntrer til at teste.

Baggrund

Tests er kun nødvendige, hvis man laver fejl! Så hvorfor ikke lave koden rigtigt fra start af? Det er nok også det de fleste af os tilstræber, men enhver erfaren programmør vil også erkende, at det er nærmest umuligt at lave fejlfri kode – i sær for store kodebaser. En tendens der ses meget i nuværende web-udvikling er, at der kommer mere og mere JavaScript til (især grundet ajax), hvorfor risikoen for fejl og behovet for test stiger tilsvarende.

Men hvorfor udførers der så ikke mere automatiseret test? Der kan være mange årsager, og jeg tror de mest dominerende er:

  • Meget af det eksisterende JavaScript er spaghetti-kode, det er simpelthen for sammenflettet til at udføre organiseret test.
    Den funktionelle programmeringsstil sammen med alt for mange annonyme funktioner, gør dette mere udtalt.
  • Skiftende programmører, sammen med at mange webudviklere har mere fokus på design end kode giver store skift i kodestil og kvalitet.
  • Alt for ofte udvikles hjemmesider under et enormt tidspres og derfor forefindes der ofte store mængder af “copy-paste”-kode og tilhørende lappeløsninger.

Ingen af disse punkter skal ses som en kritik af hverken individer, organisationer, kodestile eller lign. Næsten alle webudviklere har været udsat for disse ting, og de skal derfor mere ses som en forudsætning. Men hvorfor så automatiseret test?

Hvorfor?

Ren kode

For at kunne teste er det også nødvendigt at have ren kode. Selv om det kan være et stort arbejde at refaktorere en eksisterende kode til testbar kode, giver det pænere kode. Koden bliver typisk mere overskuelig, grundet den opdeling test tvinger en til. Ren kode er nemmere at vedligeholde og udvikle videre fra.

Dokumentation

Når funktionerne er veltestede fungerer testene også som dokumentation. De kan blandt andet bruges til at se eksempler på anvendelse og giver et dybere indblik i hvad man forventer funktionerne gør. Ud over det fungerer tests også som dokumentation for, at ens kode opfører sig som forventet.

Driftsstabilitet

De fleste kender scenariet, der er fundet en kritisk fejl, eller der er en anden hasteopgave på systemet. Man får løst fejlen, alt ser fint ud, lægger siden op og bang! Et andet kritisk system er gået ned. Mange gange designer vi JavaScript-funktioner med henblik på genbrug, især fordi “duck typing” gør genbrug nemt. Det gør det bare også nemt at rette i noget kode og så opnå uforudsete sideeffekter. En automatiseret test inden man lægger siden op hjælper med at opdage sådanne fejl, inden det går galt.

Refaktorering

En grundigt testet kode kan man trygt refaktorere i. Ofte kan det være svært at gennemskue alle sideeffekter af en refaktorering, og endnu mere umuligt manuelt at teste hver eneste del af ens side efter, for at opdage disse sideeffekter. Automatiseret test hjælper med at bekræfte, at koden virker som den skal efter refaktoreringer.

Baggrund

En god investering

Ren kode gør koden nem at vedligeholde og udvide. Derfor kan udviklerne nemmere reagere på hastesager og være mere sikker på korrektheden af løsningerne. Hurtig og korrekt løsning af problemer giver glade kunder og mere tid til at forbedre produktet.

Dokumentation gør det både nemt at få nye folk ind i projektet, da de nemt kan lære hvordan man anvender koden korrekt, og fungerer også som værdifuld dokumentation overfor ens kunder, der kan se, at man leverer det lovede.

Øget kontrol med at koden altid gør hvad man forventer, når man forventer det, skaber en mere driftsstabil side, så man ikke længere har dyre nedbrud weekenden over, hvor man risikerer at miste potentielle kunder.

Hvordan tester vi?

Under udviklingen af en hjemmeside tester man hele tiden, ofte uden at tænke over det. De fleste opdaterer siden, hver gang de ændrer i koden og bekræfter manuelt, at det virker. Denne process kan formuleres som: hvis det vi ser er det vi ønsker så er testen en success, ellers er den fejlet. Programmøren har nok opdaget, at dette er en if-sætning. Og det er kernen i hvad en automatiseret test er – der ligger naturligvis en del mere i et test-framework, men dette er grundstenen i dem alle.

Derfor kan man også uden det store besvær skrive en automatisk test. Fx på en hjemmeside, der tilføjer et element med id’et “foobar”, så kan vi med jQuery lave flg. test:

if ($(“#foobar”).length !== 1) console.log(“Testen fejlede”);

I det følgende arbejdes der med tests ud fra denne betragtning, og der introduceres ikke et egentligt test-framework – der er masser at vælge imellem og koden er triviel at omskrive til et test-framework.

Hvordan kommer vi i gang?

Mange bruger i dag jQuery, og derfor er den brugt til eksemplerne. Nogle af de hyppigt anvendte kodestrukturer i jQuery giver da også anledning til kode, der er svær at teste. I det følgende fokuseres der på at gøre en eksisterende kode testbar, da det for mange vil være første skridt til at komme i gang med TDD.

Betragt dette eksempel:

var someOuterThing = 0;
$('#doAjaxButton').on('click', function() {
    $.ajax({
        url: "http://someAjaxService",
        success: function(data) {
            $.each(data, function(key, value) {
                var $dataDiv = $('<div />', { html: value.foo });
                someOuterThing += $value.bar;
                
                $dataDiv.on('click', function() {
                    someOuterThing += value.bar*2;
                });
            });
        }
    });
});
Lad os lige se bort fra, at der muligvis er en scoping-fejl, men se på koden ud fra et test-mæssigt perspektiv. For eksempel vil vi gerne vide, at click-handleren på det indre element regner på variablen “someOuterThing” korrekt.

Med et tilbageblik på, at en test i essensen er en if-sætning, så er første fokus at få placeret “someOuterThing”, så den kan testes. Lige nu har vi ingen kontrollerede steder, hvor man kan teste den, da den er nested ind i kald, som køres på ukendte tidspunkter. For at køre en if-sætning skal vi også vide hvilket resultat vi får, og derfor er ajax-kaldet også en faktor, der skal væk fra testen.

I teorien kunne vi skrive testen inde i kaldet og simulere ajax-data, dette bliver dog hurtigt besværligt og uoverskueligt, hvilket ikke hjælper kodekvaliteten. En anden og nok lidt smartere fremgang er at refaktorere funktionen til click-handleren ud i en separat variabel, da den så kan køres uafhængigt af brugerinput og ajax-kald:

var someOuterThing = 0;
var dataDivClickHandler = function() {
    someOuterThing += value.bar*2;
};
 
 
$('#doAjaxButton').on('click', function() {
    $.ajax({
        url: "http://someAjaxService",
        success: function(data) {
            $.each(data, function(key, value) {
                var $dataDiv = $('<div />', { html: value.foo });
                someOuterThing += $value.bar;
                
                $dataDiv.on('click', dataDivClickHandler);
            });
        }
    });
});

Lige nu ses der bort fra, at variablen “value” ikke kan bruges fra funktionen. I stedet kigges der på if-betingelsen, der udgører test-scenariet. For at teste om funktionen virker, skal vi teste om “someOuterThing” giver det forventede i et kontrolleret situation. Sættes “value.bar=2;” kan vi hurtigt se, at det forventede resultat er 4, hvilket kan testes som følger:

var someOuterThing = 0;
var value = { var: 4 };
 
var dataDivClickHandler = function() {
    someOuterThing += value.bar*2;
};
 
dataDivClickHandler();
if (someOuterThing === 4) console.log("Hurray, it works!");
else console.log("Oh bummer, we need to find the error");
 
 
$('#doAjaxButton').on('click', function() {
    $.ajax({
        url: "http://someAjaxService",
        success: function(data) {
            $.each(data, function(key, value) {
                var $dataDiv = $('<div />', { html: value.foo });
                someOuterThing += $value.bar;
                
                $dataDiv.on('click', dataDivClickHandler);
            });
        }
    });
});

Eksemplet har to markante fejl:

  1. Det er meget besværligt at teste flere værdier, og i tests bør man ofte være opmærksom på grænseværdier, fx. 0
  2. Der er lige nu scoping-problemer i koden

Det vil derfor være favorabelt at få value ind i funktionen som en parameter. I jQuery er der en dataparameter, som sendes til funktionen som en parameter. Navngiver vi parameteren “event” vil dataene ligge i “event.data”. Det resulterer i:

var someOuterThing = 0;
var event = { data: { var: 4 }};
 
var dataDivClickHandler = function(event) {
    var value = event.data;
    someOuterThing += value.bar*2;
};
 
dataDivClickHandler();
if (someOuterThing === 4) console.log("Hurray, it works!");
else console.log("Oh bummer, we need to find the error");
 
 
$('#doAjaxButton').on('click', function() {
    $.ajax({
        url: "http://someAjaxService",
        success: function(data) {
            $.each(data, function(key, value) {
                var $dataDiv = $('<div />', { html: value.foo });
                someOuterThing += $value.bar;
                
                $dataDiv.on('click', value, dataDivClickHandler);
            });
        }
    });
});

Skulle eksemplet føres videre skal funktionen skrives yderligere om, så den returnerede data til “someOuterThing”, i stedet for at regne på den direkte og der vil sikkert også være plads til andre forbedringer. Eksemplet stoppes dog her, da funktionen i bund og grund er gjort testbar og nu også kan genbruges.

Skulle testen viderudvikles er der også fejl i den feedback der gives, når koden køres. Fejlene er her bevidst ignoreret, da et test-framework tager hånd om at levere bedre feedback

Selv om artiklen her ikke beskriver TDD i sig selv, så er tankegangen her ikke kun relevant for at omdanne gammel kode til test-venlig kode, men også i forhold til hvordan ny kode skrives.

Tankegangen kan opsummeres til:

  • Skriv funktioner i variable eller definer dem med et sigende navn, så vi kan nå dem flere steder i koden og teste dem uafhængigt af hvor de bruges
  • Sørg for at input er parametre, så man kan styre hvad man regner på
  • Få resultatet på en måde så man kan teste det, helst gennem returværdier. Brugen af globale variable er kraftigt frarådet.

Herfra har man så et udgangspunkt for faktisk at komme i gang med TDD.