Testdrevet utvikling i Javascript, del 1
JavaScript er like god kandidat som et hvilket som helt annet språk for testdrevet utvikling. Gjennom tre artikler skal vi ser litt på hva som ligger i testdrevet utvikling, hvordan vi kan gjøre det i JavaScript, og hvilke rammeverk som er tilgjengelig.
Følgende er en kort introduksjon til konseptene, men langt fra noen fullstendig referanse på temaet. I slutten av denne artikkelen finner du flere nyttige lenker dersom du er interessert i temaet.
Enhetstesting
Unit testing is a method of testing that verifies the individual units of source code are working properly.
Med andre ord: kode som tester annen kode. Fordelen ved å kjøre en enhetstest fremfor å kjøre programmet/koden i nettleseren/konsoll e.l er at enhetstesten kan kjøres igjen og igjen uten ekstra innsats. Når kodebasen blir stor og testene mange betyr dette at du kan gjøre endringer "dypt" i koden, og så kjøre alle testene for å verifisere at du ikke har ødelagt noe du skrev for to uker siden.
Dersom du er flink til å dekke opp alle eventualiteter i koden med slike tester kan du drastisk begrense tiden du bruker på manuell testing og graving i konsollet. Tiden du bruker på testing blir da en bedre investering for fremtiden.
En "enhet"
Med en enhet menes den minste testbare enheten som er tilgjengelig. I objektorienterte språk er det stort sett metoder/funksjoner. Altså er en enhetstest i JavaScript et stykke kode (en funksjon) som tester en metode/funksjon.
Enhetstester som retter seg mot samme funksjon eller gruppe funksjoner grupperes gjerne i et test case (vanligvis en "klasse"), og disse grupperes gjerne sammen i test suiter. Jeg kommer tilbake til disse uttrykkene når vi ser på konkrete rammeverk.
Kodedekning
Kodedekning ( code coverage) er et mål på i hvilken grad kildekoden vår er testet. I praksis betyr det at man kan vurdere hvor godt en test suite "dekker" koden den er ment å test.
Idéellt sett vil en test suite kjøre gjennom all kode som testes. Dette innebærer å kalle alle metoder, dekke alle grener i if-tester, og mer. Det er vanlig å definere et sett med dekningskriterier. Jeg siterer fritt fra Wikipedia:
- Function coverage - Blir alle funksjoner/metoder i programmet kjørt?
- Statement coverage Bli hver enkelt linje med kildekode kjørt?
- Decision coverage/Branch coverage Har alle kontrollstrukturer evaluert både til
trueogfalse(evt, gått inn i alle greiner)? - Condition coverage - Har alle boolske deluttrykk evaluert både til
trueogfalse? - Path coverage Blir alle mulige veier gjennom koden kjørt?
- Entry/exit coverage - Har alle kall mot en funksjon og returer fra en funksjon blitt testet?
Merk at det ikke er gitt at du skal ha 100% dekning for alle disse kriteriene. Path coverage er for eksempel i gitte tilfeller umulig å dekke 100%. I praksis måler man dekning med et verktøy som analyserer koden og testene dine.
En simpel enhetstest
Ok fint, da vet vi hva en enhetstest er, hvordan ser den ut?
function add(a, b) {
return a + b;
}
// Test funksjonen
if (add(1, 2) !== 3) {
document.write('<p style="color: red;">add(1, 2) blir ikke 3</p>');
} else {
document.write('<p style="color: green;">add(1, 2) OK</p>');
}
if (add("2", 2) !== 4) {
var msg = 'Tall som strenger lar seg ikke addere som tall';
document.write('<p style="color: red;">' + msg + '</p>');
} else {
document.write('<p style="color: green;">add("2", 2) OK</p>');
}
Kjører vi dette (kopier og lim inn i Firebug og kjør, evt kjør denne) får vi en grønn linje og en rød linje. Dette er veldig simple eksempler, men illustrerer noen av de viktigste aspektene ved en enhetstest:
- Metoden testes fra en eller flere "angrepsvinkler" (i dette tilfellet med forskjellige typer input)
- Vi kontrollerer resultatet (returverdien) mot det vi forventer at metoden skal returnere for denne inputen
- Stemmer resultatet overens med våre antagelser gir vi tommelen opp
- Dersom resultatet avviker med antagelsen vises en feilmelding
Når vi får en feilmelding retter vi feil i koden og kjører testene på nytt:
function add(a, b) {
return parseInt(a) + parseInt(b);
}
if (add("2", 2) !== 4) {
var msg = 'Tall som strenger lar seg ikke addere som tall';
document.write('<p style="color: red;">' + msg + '</p>');
} else {
document.write('<p style="color: green;">add("2", 2) OK</p>');
}
Denne passerer, og koden vår er da i henhold til de forholdsvis enkle testkriteriene våre.
Du ser fort av disse eksemplene at det er mange gode muligheter til å generalisere litt. Det har lite for seg å skrive logikk for testing, visning av resultat/feilmeldinger osv. Av den grunnen bruker vi et enhetstesterammeverk til å skrive enhetsteser i, noe som er tema for del 2 av denne artikkelen (i morgen). Rammeverkene varierer i hvordan de løser ting, men det er vanlig at et testrammeverk byr på såkalte assertions - funksjoner som brukes for å anta noe om koden vår. I eksempelet over ser vi slike assertions i form av if-testene mine, men dette er som nevnt på et svært lavt nivå.
Testdrevet utvikling
Med enhetstestene som vårt verktøy er vi klare til å snu opp ned på utviklingsmetodikken vår, og ta inn over oss konseptet som er testdrevet utvikling. Testdrevet utvikling foregår i en syklus og fungerer grovt sett på følgende måte:
- Skriv en test for en ny feature
- Kjør alle testene, og verifiser at den nye testen feiler
- Skriv en minimal løsning som får testen til å passere (her er det lov å slurve, så lenge testen passerer)
- Refaktorerer - rydd opp og puss på løsningen
- Lather, rinse, repeat
Med testdrevet utvikling beskriver vi først (med testene) hvordan vi ønsker at koden vår skal fungere. Deretter kan vi bekymre oss for hvordan vi skal komme i mål med dette. Dette betyr at enhetstestene er en sentral del av designet av koden vår. Så fort du kommer inn i vanen med å jobbe på denne måten vil du oppdage at dette påvirker kodedesignet ditt i positiv retning. Fokuset ditt er nå på API-et fremfor implementasjonen.
Bonusen for JavaScript
En av de aller største utfordringene med å skrive noe som helst utover moderat komplisert JavaScript er cross browser-problemer. Disse utfordringene sparker deg kontinuerlig i ræva på to måter:
- Du bruker mer tid på å utvikle kode fordi koden må fungere likt i så mange forskjellige miljøer
- Du får en lite smidig arbeidsflyt fordi du jevnlig må innom flere nettlesere for å verifisere koden din
Enhetstesting hjelper deg her på en måte som er noe spesielt for JavaScript. Det er sant at enhetstester også i andre språk hjelper deg å verifisere om feks kode for Ruby 1.8 kjører på Ruby 1.9, men for JavaScript er miljøene flere og langt mer sidestilte.
Når du skriver enhetstester som kan kjøres igjen og igjen er det ikke noe problem å sitte dagen lang med din favorittnettleser. Når du ønsker å sjekke av mot de andre nettleserne har du jo alle testene dine, skritt for skritt, og skulle det oppstå problemer er det sannsynlig at du lokaliserer dem raskt og kan løse mange små problemer i en jafs fremfor å miste 5-10 minutter (og dermed flyten) hver gang IE driter seg ut på de mest banale måter.
"Men det er vanskelig og tar lang tid"
Ja. Til å begynne med gjør det det. Du kan trøste deg med at det tar ikke lang tid før du har vent deg av med ad hoc-testing i nettleseren og heller investerer tiden din i å skrive tester som kan kjøres igjen og igjen - noe som sikrer kvaliteten på koden din både nå og senere. Du kan også trøste deg med at du sannsynligvis tester koden langt mer grundig og systematisk på denne måten enn om du skulle simulert diverse oppførsel på egenhånd.
Testing krever trening. Til å begynne med er det veldig uvant, men etterhvert får man det i fingra og da blir det morsomt! Når det gjelder testdrevet utvikling er dette noe heller ikke jeg er noen ekspert på, og jeg trener hele tiden for å bli bedre.
Som trening både for dere og meg kommer resten av julekalenderen til å aktivt bruke enhetstester. Med andre ord: en testdrevet julekalender om JavaScript.
Vel møtt til Testdrevet utvikling i JavaScript, del 2 i morgen, der jeg tar for meg noen aktuelle rammeverk.
Kommentarer
Fredrik Helgesen
4. desember, 09:50
Så deilig det er med en skikkelig geeky julekalender, syns den er lærerik siden det er begrenset hvor mye JS jeg får brukt i min jobb :)
Marius Mathiesen
(http://shortcut.no/)
4. desember, 09:50
Denne burde virkelig være tilgjengelig på andre språk også, selv om Google gjør en nesten bra jobb: http://translate.google.com/trans...=en&ie=UTF-8&sl=no&tl=en
Christian
4. desember, 10:39
Vanskelig å unngå norsk-engelsk når man oversetter fremfor å skrive fra levra. Øvelse makes master.