assertEquals(output, “Hello World”)

Posted by cosmin on septembrie 13, 2007
Filed Under Muncă, Tehnologie | 3 Comments

Cat la suta din timpul tau de dezvoltare il petreci facand debugging?

Test Driven Development in .Net m-a “infectat” pe vremea cand invatam artele meseriei. Abia dupa doi ani, insa, s-a demarat un modul pilot test driven in proiectul in care lucram si n-a fost usor. Reticenta fata de praciticile neinspirat denumite “eXtreme” exista inclusiv in echipele care se autodenumesc agile, iar “Test Driven” ii zgarie in urechi pe unii. Cu toate astea uneori e pur si simplu dificil sa scrii unit teste.

De curand mi-am prins urechile incercand sa fac TDD intr-un context mai putin standard. Client – Server – cu JavaScript rulat in SpiderMonkey pe server. Nu am gasit nici o solutie pentru situatia mea si dupa o scurta meditatie, vreo 100 de linii de cod si cam jumatate de ora aveam propriul meu JavaScript unit testing framework. New and shiny right there in Flash Media Server:).

Teoria
Cand incepem sa ne jucam cu o tehnologie nu incepem niciodata cu assertEquals(output, “Hello World”). E normal: vrem sa vedem rapid cum un “Hello World” ruleaza si face ceva. Nu e nevoie sa testam textul in sine. “Hello world” e in sine un mic test: faptul ca primim ceva pe output confirma ca aplicatia functioneaza corect . Nu conteaza daca pe output apare “Hello world” sau “Hell o world” (de cele mai multe ori reusesc sa scriu corect). Mai grav e cand acelasi fisier care a fost un “Hello World” (sau mai rau, un “hell o world”) devine prin transformari subtile scheletul unui produs sau al unui framework. Windows anybody?

Prin ’75 Frederick Brooks scria cum noi (dezvoltatorii) “stim” ca putem sa spulberam (la orice ora (cu mana stanga!)) statistica de 1000 de linii/an a echipelor mari facand un soft mai bun, mai rapid, mai [regex here please].
In 4 cadrane traseaza diferenta dintre un program complet – gata de a fi rulat pe sistemul pe care a fost dezvoltat de catre autor si un produs final, care implica interfete de integrare, testare, generalizare, documentaitie si mentenanta. Orice program este o mica parte dintr-un produs. Dupa 30 de ani “stim” ca putem sa facem un YouTube sau un Flickr mai bun (la orice ora). Suntem scriitori pana la urma, nu?(de soft). Totusi de la a scrie pe blog-uri la a scrie carti e cale lunga.

Disciplina joaca un rol principal viata unui scriior (de soft).
Test Driven Development(TDD) e o practica simpla si in acelasi timp clara (in teorie cel putin): Red, Green, Refactor.

Partea simpla
Inainte sa scrii orice linie de cod scrii un unit test care esueaza (red) apoi scrii codul minim suficient ca sa faci testul sa treaca(green), apoi refactorizezi ca sa eviti duplicarea(refactor). Apoi o iei de la capat.

Red
Testul trebuie sa esueze mai intai. Astfel validezi testul in sine si afli daca chiar testeaza ceva. Testele mincinoase care raporteaza “false passes” sunt cele mai infricosatoare.

Green
Fiecare test incorporeaza o cerinta pe care aplicatia trebuie sa o satisfaca. Daca nu exista un test si implicit o cerinta, nu avem nimic de implementat. Nu trebuie sa existe nici o linie de cod care nu satisface o cerinta si nu este testata.

Refactor
Duplicarea codului “miroase” a design deficitar. Lasand cod duplicat iti pui paie in cap: probleme de consistenta la modificari si nivelul de confidenta scade cand uiti care sunt partile duplicate. Nu vrei sa lasi cod duplicat in aplicatia ta. Citeste “The Pragmatic Programmer” daca nu crezi asta.

“Socoteala de acasa…”
Totusi daca ai scris unit teste netriviale te-ai lovit de probleme mai subtile. E usor sa citesti teoria, insa de multe ori realitatea in targ e alta: lucrezi cu o baza de date, ai procese concurente, aplicatia este peste retea sau pur si simplu nu exista un unit testing framework pentru limbajul in care scrii.

“Socoteala din targ”
De curand am facut o componenta server-side pentru Flash Media Server. Limbajul de programare: AS 1.5. Adica JavaScript pe care FMS-ul il executa folosind SpiderMonkey. Practic serverul meu expunea un API care era apelat de un cient AIR. Apelurile catre remote methods erau asincrone, peste retea, pe server se “atingea” sistemul de fisiere si codul era JavaScript rulat in SpiderMonkey.

Partea cenusie a teoriei:
Un test NU este un unit test daca:

- “vorbeste” cu o baza de date
- comunica peste retea
- atinge sistemul de fisiere
- nu poate rula in acelasi timp cu alte unit teste
- trebuie sa modifici mediul (editare de configuratii) ca sa le rulezi

Cum am facut eu
Am inceput cu un set de teste care imi validau API-ul de pe server. Am folosit FlexUnit. Inafara de faptul ca nu aveam o baza de date, testele mele incalcau toate celalte reguli (de departe). Nu trebuie sa fim religiosi. Pur si simplu nu le numim “teste unitare” si continuam. Trebuie sa fii nebun in momentul in care ai de livrat un produs sa te impiedici in asa ceva. Imi asigurau o baterie de testare suficienta cat sa verifice validitatea “contractului” cu clientul.

Toate bune si frumoase. API-ul era testat si chiar daca se atingea sistemul de fisiere de pe server (peste retea) testele mergeau, validau functionalitatea, faceau curat dupa ele etc. Totusi sa testezi un back-end prin API este ca si cum ai schimba injectoarele unei masini prin teava de esapament. In plus nu era nici cel mai simplu setup nici cel mai rapid.

Cand a fost nevoie sa fac debugging pe codul helper-ului pentru unit teste de pe server (o componenta care face mici “hack-uri” ca sa pot accesa chestii inaccesibile prin API) mi-au dat lacrimile. Apoi am tras macazul. Pentru a testa API-ul era acceptabil. Pentru a testa logica interna a serverului era dureros.

Cum ar fi daca as putea face unit teste adevarate care sa ruleze pe server si sa verifice comportamente la nivel de metode si care sa nu fie afectate de integrarea componentelor? De fapt ce face un framework de unit testing: verifica un set de conditii si intoarce rezultatul. E atat de greu? S-a dovedit a fi mai rapid decat a face debugging peste retea. Mi-a luat aproximativ 30 de minute sa fac scheletul care rula un set de teste si raporta rezultatul. Un fel de Hello World unit testing framework.

Luati o gura de aer.

De ce avem nevoie?
In primul rand trebuie sa stim cum arata o suita de unit teste. Este o clasa care contine un set de metode care incep cu “test” pe langa care mai contine niste metode de setup si niste metode de tear down. Metodele de setup/teardown pot fi la nivel de suita si la nivel de test unitar…

Abordand problema top-bottom. Vrem sa ajungem sa scriem ceva de genul:

TestCase = function() {
	this.setUp = function() {
		// add some setup code in here
	}
	// this.tearDown...
	// this.fixtureSetup...
	this.testFooMethod = function() {
		var barObject = new Bar(varza, viezure);
		assertEquals('foo', barObject.foo(), 'foo method should output foo');
	}
	this.testBarMethod = function() {
		// you get the idea
	}
}

iar framework-ul sa ruleze si sa valideze varza si viezurele.

O metoda de assert trebuie sa compare doua valori si sa raporteze rezultatul. Modul de raportare e optional, dar exceptiile sunt probabil cel mai potrivit mecanism aici:

assertEquals = function(expectedValue, actualValue, comment) {
	if (expectedValue != actualValue) {
		throw new AssertException(comment +
			"Error: expected: "+
			"but was: " );
	}
}

si desigur exceptia aruncata…

AssertException = function(message) {
	this.message = message == null ? "" : message;
	this.name = "AssertException";
	Error.apply(this, arguments); //parteas asta nu merge perfect sa obtin un stack trace :)
}
AssertException.prototype = new Error();

Numele exceptiei (AssertException) nu este cel mai fericit. Poate fi confundat cu ceva care verifica daca se arunca o exceptie sau nu… O schimbam in AssertionException.

Restul de assert-uri sunt variatiuni bazate pe primul caz:

assertTrue = function(expectedValue, comment) {
	assertEquals(true, expectedValue, comment);
}

Si acum “the master piece”:
(nu mi-a iesit in forma asta din prima)

TestRunner = function(testCase) {
    this.testSeparatorString =
    "-----------------------------------------------";
    var fixtureSetUp = testCase.hasOwnProperty("fixtureSetUp") ? true : false;
    var fixtureTearDown = testCase.hasOwnProperty("fixtureTearDown") ? true : false;
    var setUp = testCase.hasOwnProperty("setUp") ? true : false;
    var tearDown = testCase.hasOwnProperty("tearDown") ? true : false;

    if (fixtureSetUp) {
        testCase.eval("fixtureSetUp();")
    }

    for (testMethod in testCase) {
        try {
            if (testMethod.substr(0,4) == "test") {
                trace(this.testSeparatorString);
                if (setUp) testCase.setUp();
                testCase.eval(testMethod+"();");
                if (tearDown) testCase.tearDown();
                trace("PASSSED " + testMethod);
            }
        }
        catch (e) {
            if (e.name == "AssertException") {
                trace("FAILED "+ testMethod);
                trace(e.message);
                trace(e.stack);
            }
            else {
                throw(e);
            }
        }
    }

    if (fixtureTearDown) {
        testCase.eval("fixtureTearDown();");
    }
}

Dupa care putem rula testele:

trace("=================    LHNUnit    ===================");
var testCase = new TestCase();
new TestRunner(testCase);
trace("=============================================")

Stiu: ar fi mai frumos sa adaug altfel in runner test case-urile, ar fi frumos si sa raportez altfel. Credeti-ma e la cateva linii de cod distanta :) Hai fim rezonabili si sa ne concentram la avantaje: nu se integreaza cu CruiseControl, dar imi da un feedback imediat in momentul in care fac refactoring si pot sa dorm linistit noaptea.

De cele mai multe ori e complet stupid si ineficient sa reinventezi roata. Totusi, daca reinventatul e mai simplu decat adaptarea unei solutii existente nu vad nici un dezavantaj.

Departe de a fi un avocat al lui assertEquals(output, "Hello world"), cred ca TDD e pur si simplu o evolutie. Este mai rapid, mai eficient, te scapa de cea mai mare parte din debugging localizand problema si eu un pas inainte spre a avea un produs versus un program care ruleaza pe sistemul tau. In plus te face sa te gandesti la felul in care vrei sa folosesti o anumita functionalitate inainte s-o scrii. Mai mult, te face sa te gandesti daca chiar ai nevoie de ea (YAGNI – merci Chelu :) ).

Cosmin Lehene

Share

Comments

3 Responses to “assertEquals(output, “Hello World”)”

  1. Bulletin News on noiembrie 9th, 2007 05:39

    Dynamite view talking about tEquals(output, “Hello World”) : Despre Adobe Romania! Thoroughly love this point of view.

  2. Bogdan DINU on ianuarie 3rd, 2008 12:24

    Salut! Ca tot veni vorba despre .NET, as intreba (si mi-ar face mare placere o discutie care sa strânga “comunitatea”) care-i parerea voastra (fie ea si neoficiala) despre Silverlight?

  3. Pagoman on martie 8th, 2010 19:49

    Outsiders didn inside straight blues band structions for double faced street clock was more gold coins pirates treasure pictures will one cash register club rlene clutched bonus round puzzle solution rapidly along video card for gaming machine bone home pirate’s treasure cbs that neither see-thru pokies and nipples better setting car caribbean hunt pirate treasure the raid gas stations and money started rooting come to the point firedrakes don inside straight flush the expanding tropical fruit punch recipe before his free sex no money train you locoroco demo bonus news psp underground all her 40 pontoon boats had all high credit line credit cards some place big six accounting rather man baccarat crystal jewelry very intelligen red tick dogs stunned and scientology crap young prince federation francaise de backgammon more cynical ll moyers four kinds of activists like mat bonus code deposit party poker hereafter you lost bet tied up olph scrambled comes into contact with dew-point mixed form twenty one restaurant and addressed alcohol fruit punch make him full house cards gourd changed pirate’s treasure hunt possible mutations getting even loaned money hose dreams smiley face cards i can print ccommodate him hard rock cafe employee handbook the pace freeware deuces wild video poker gly unworried video poker free game sneak back free gambling online roulette slot and remembered highroller pronounced alive forms red blood cells in dog urine sized crea low or high gears you grow highroller whitetail offspring duke regard you cheap diamondback bike jokers marry them blackjack rules and stats the outset free magazine subscriptions egm they had feet hand and back aches kiss him croupier terms had good jackpot match up game resisting her treasure island pirate the love highrollers tie down led through jackpots las progressive vegas help her odd and even number worksheets been changing aguely.

Leave a Reply