Tesztvezérelt fejlesztés

Tesztvezérelt fejlesztés JavaScriptben

Egy konkrét példán keresztül szeretném bemutatni, hogy miként lehet JavaScriptben tesztvezérelt fejlesztés szerint programozni.

Mit jelent a tesztvezérelt fejlesztés?

A tesztvezérelt fejlesztés (angolul: Test Driven Development, röviden: TDD) egy szoftverfejlesztési technika. Szerencsére ma már magyarul is számtalan cikk foglalkozik a témával, emiatt az elméleti hátterét csak nagyon röviden szeretném ismertetni. A TDD nem tesztelési, hanem fejlesztési módszertan. A lényege, hogy előbb készítjük el a tesztet, majd utána a neki megfelelő kódot. A tesztvezérelt fejlesztés menetét legegyszerűbben úgy tudjuk megfogalmazni, hogy a fejlesztés irányát a tesztek határozzák meg, első lépésként a specifikációt tesztekké alakítjuk, majd megírjuk azt a kódot, amely kielégíti a teszteket.

Mi ellen nem véd?

Tehát a TDD-ben a specifikációt alakítjuk át tesztekké, majd a tesztekben elvárt működést valósítjuk meg. Ám ez nem feltétlen azt jelenti, hogy ezután kizárólag hibátlan kódot készítünk, hanem azt, hogy a specifikációnak megfelelő programot írunk. Rosszul megfogalmazott specifikáció, rosszul megírt kódot eredményez.

Miért jó ez mégis nekünk?

A tesztek segítenek abban, hogy bármikor, bátran módosítsuk a programunkat. Nem kell attól félni, hogy egy-egy utólagos módosítás elrontja a már meglévő programunkat. Ha módosul a specifikáció, akkor a teszteseteket kiegészítjük, esetleg átalakítjuk, majd utána elvégezzük a kódon a szükséges módosításokat. Végül, ha minden teszt hibátlanul lefut, akkor biztosak lehetünk abban, hogy semmit sem rontottunk el. Szerintem nem lehet azt kellően kihangsúlyozni, hogy fejlesztés közben mekkora segítség az, hogy a programunk helyes működését bármikor, pillanatok alatt ellenőrizni tudjuk.

A konkrét feladat

Hosszan kerestem, hogy mi lenne az ideális feladat, amivel a TDD-t be lehet mutatni. Ha túl egyszerű kódon mutatom be a tesztelést, akkor mindenki csak legyint. Mi értelme az egyértelműen helyes függvényről bebizonyítani, hogy tényleg helyes? Ám, ha túl komplex a példa, akkor nehezen látszódik a lényeg. Remélem, sikerült megtalálni a középutat... Egy nagyon egyszerű sablonrendszert fogunk készíteni JavaScriptben. Megadunk egy sablont, megadjunk a behelyettesítendő értékeket, végül kinyerjük az eredményt.
var tpl = new JSTemplate(
    '<div class="{$cls}">',
        '<foreach name="$users">',
            '<a href="?id={$id}">{$name} ({$age})</a><br />',
        '</foreach>',
    '</div>');
tpl.append({
    'cls': 'users',
    'users': [{
        'id': 1,
        'name': 'John',
        'age': 21
    }, {
        'id': 2,
        'name': 'Bob',
        'age': 30
    }]
});
document.getElementById('ct').innerHTML = tpl.fetch();
// <div class="users"><a href="?id=1">John (21)</a><br /><a href="?id=2">Bob (30)</a><br /></div>
Az alapötletet az ExtJS 1.0.1 Ext.Template osztályából vettem. Hogy ne vesszünk el a részletekben, az ottani kódot jelentősen egyszerűsítettem.

Specifikáció

Alapértelmezetten a sablont egy string-ként várjuk, de lehetőséget kell biztosítani tetszőlegesen több string megadására is, illetve string-eket tartalmazó tömböt is adhassunk át. Ezekben az esetekben a string-ek összefűzése legyen az alap sablon. A változókat {$varName} alakban kell megadni, lehessen használni módosítókat {$varName:upper}, illetve lehessen paramétereket is átadni {$varName:substr(1, 2)}. A módosítókat külön kell kezelni, lehetőséget kell biztosítani, hogy azokat az eredeti kód módosítása nélkül is bővíthessük. Listák feldolgozására is szükségünk van, ezeket a <foreach name="$varName">...</foreach> alakban lehessen megadni. Első körben, a listában listákkal nem foglalkozunk. Az osztály neve legyen JStemplate, a konstruktor paraméterébe adjuk meg a sablont. Legyen egy append nevű függvény, ahol egy objektumban átadjuk az összes változót, a fetch adja vissza a behelyettesítés eredményét.

Ismerkedés a Jasmine framework-kel

Mielőtt nekikezdünk a munkának, gyorsan nézzük meg, hogy JavaScript alatt milyen segítséget kapunk az egységtesztek készítésére. Jelen példában a Jasmine nevű rendszert fogjuk használni, számtalan függvényt kapunk segítségül, nekünk a jelenlegi feladathoz elegendő lesz csak néhánnyal megismerkedni: Természetesen ez a teljes függvénylistának csak egy apró töredéke. Nekünk most ennyi pont elégendő lesz, de fontos kiemelni, hogy a Jasmine többek között a gazdag eszközkészlete miatt az egyik legnépszerűbb JS test framework. Fejlesztés közben majd két fájlt fogunk szerkeszteni, az elsőben a sablon rendszerünket építjük, míg a másodikban a hozzá tartozó teszteket. A teszteket tartalmazó HTML oldal felépítését is a Jasmine rendszerből vettem. A hosszúra nyúlt bevezető után már tényleg nem maradt más hátra, minthogy elkezdjük a sablonrendszerünk fejlesztését!

Az első tesztek

Ha TDD szerint programozunk, akkor első lépés a legrövidebb teszt elkészítése, ami hibára fut. Ez legtöbb esetben nem más, mint a nem létező osztály példányosítása.
describe('init', function () {
    it('Start', function () {
        new JSTemplate();
    });
});
Ha megnézzük a tesztoldalt, akkor láthatjuk, hogy tényleg hibás. A teszt javítása sem lesz hosszabb:
var JSTemplate = function () {};
Gondolom, most többekben felmerül az a kérdés, hogy ennek van-e bármi értelme? Két dolgot vegyünk figyelembe. Először is, eddig alig tíz másodpercet kódoltunk, így ne várjunk még túl nagy eredményeket. Másodszor, ez tényleg csak az első lépés volt. Nézzük tovább a specifikációt, ha egy változó nélküli sablont adunk át, akkor egy az egyben ugyanazt szeretnénk visszakapni. Az előző tesztet írjuk át.
describe('init', function () {
    it('Kezdeti parameteratadas', function () {
        var str = 'Szia vilag!', t;
        t = new JSTemplate(str);
        expect(t.fetch()).toEqual(str);
    });
});
Gyors váltás a böngészőre, ahol láthatjuk, hogy az új tesztünk hibás lett. Nézzük a hozzá tartozó kódot.
var JSTemplate = function (html) {
    this.html = html;
};
JSTemplate.prototype = {
    fetch: function () {
        return this.html;
    }
};
Jelenleg még nagyon egyszerű a kódunk, emiatt a refaktorálásra még nincs szükség, de ne feledjük, hogy az iteráció utolsó lépése a refaktorálás! Következő lépésben vegyük azt az esetet, amikor egy tömböt adunk át:
...
t = new JSTemplate([str, str]);
expect(t.fetch()).toEqual(str + str);
A hozzá tartozó megoldás:
var JSTemplate = function (html) {
    if (html instanceof Array) {
        html = html.join('');
    }
    ...
Végül, amikor több paramétert adunk át:
...
t = new JSTemplate(str, str);
expect(t.fetch()).toEqual(str + str);
...
És a gyors megoldása:
...
} else if (arguments.length > 1) {
    html = Array.prototype.join.call(arguments, '');
...
A cikkben igyekszem csak azokat a forráskód részleteket leírni, amik módosulnak, de hogy biztosan követhető legyen a folyamat, feltöltöttem a GitHub-ra a teljes kódot, a fontosabb állomásoknál egy-egy commit-tal.

Reguláris kifejezések

Ha ezzel megvagyunk, akkor térjünk át a reguláris kifejezésekre, a string-ekből szedjük ki a változókat. Én a reguláris kifejezésekhez mindig készítek teszteket, hiába privát elem, mégiscsak ez a legkönnyebben elrontható része a kódnak, ám a legkönnyebben tesztelhető is. Szükségünk van a teljes mintára, illetve a változónévre:
describe('regexp1', function () {
    var template = new JSTemplate(),
        re = template.variableRegexp;

    it('Szuro nelkuli vatlozok', function () {
        var str = '<div id="{$id}" class="{$class}">{$content}</div>',
            matches = [],
            names = [],
            match;
        while ((match = re.exec(str)) !== null) {
            matches.push(match[0]);
            names.push(match[1]);
        }
        expect(matches).toEqual(['{$id}', '{$class}', '{$content}']);
        expect(names).toEqual(['id', 'class', 'content']);
    });
});
A neki megfelelő reguláris kifejezés:
JSTemplate.prototype = {
    variableRegexp: /\{\$(\w+)\}/g,
    ...
Várakozásunknak megfelelően a teszt hibátlanul lefut.

Változó behelyettesítés

Most választhatunk. Vagy folytatjuk a reguláris kifejezés továbbfejlesztését és elkészítjük a módosítókat, majd a paramétereket vagy a sablonrendszert elkészítjük módosítók nélkül. Az utóbbit javaslom. Nézzük a következő tesztet:
describe('simple', function () {
    it('Egyszeru valtozok 1.', function () {
        var str = 'Szia {$name}!',
            obj = {
                'name': 'vilag'
            },
            result = 'Szia vilag!',
            t = new JSTemplate(str);
        t.append(obj);
        expect(t.fetch()).toEqual(result);
    });
});
Kezd hasonlítani a cikk elején lévő példához. Készítsük el gyorsan a hozzá tartozó kódot is:
JSTemplate.prototype = {
    ...
    append: function (values) {
        this.values = values;
        return this;
    },
    fetch: function () {
        var me = this, re = this.variableRegexp;
        return this.html.replace(re, function (match, name) {
            return me.values[name];
        });
    }
};
Közben ne felejtsük el, hogy...
  1. elkészítjük a tesztet
  2. megnézzük, hogy valóban hibát jelez
  3. megírjuk a kódot
  4. ismét megnézzük, hogy kijavult-e a teszt
  5. ha szükséges, akkor refaktorálás, kódszépítés
  6. refakorálás után újra megnézzük, hogy nem rontottunk-e el valamit

További tesztek

Sokan mindig csak annyi tesztet készítenek, amennyi feltétlenül szükséges. Ebben a kérdésben én engedékenyebb vagyok, alkalmanként készítek olyan teszteket, amelyek már az elkészítésekor is helyesen lefutnak. Ha később törik a kód, akkor előfordulhat, hogy pontosabb képet kapunk a hiba okáról. A refaktorálás részeként a teszteket bővítem.
...
it('Egyszeru valtozok 2.', function () {
    var str = '<div id="{$id}" class="{$cls}">{$content}</div>',
        obj = {
            'id': 'v1',
            'cls': 'title',
            'content': 'Hello!'
        },
        result = '<div id="v1" class="title">Hello!</div>',
        t = new JSTemplate(str);
    t.append(obj);
    expect(t.fetch()).toEqual(result);
});
it('Egyszeru valtozok 3.', function () {
    var str = '{$foo}{$foo}{$foo}{$boo}',
        obj = {
            'foo': 'f',
            'boo': 'b'
        },
        result = 'fffb',
        t = new JSTemplate(str);
    t.append(obj);
    expect(t.fetch()).toEqual(result);
});
...

Nem létező paraméter

Az előbbi tesztgyártás közben jutott eszembe, hogy készítek egy olyan tesztet, ahol a nem létező paramétert vizsgálom. Az elvárás az üres string lenne, ezzel szemben az eredmény az 'undefined' string lett. A specifikáció hiányosságait alkalmanként néhány "felesleges teszt" pótolhatja. A teszt:
...
it('Undefined', function () {
    var str = 'Szia {$nev}!',
        obj = {
            'name': 'vilag'
        },
        result = 'Szia !',
        t = new JSTemplate(str);
    t.append(obj);
    expect(t.fetch()).toEqual(result);
});
...
A hozzá tartozó kód:
...
return this.html.replace(re, function (match, name) {
    return me.values[name] || '';
});
...

Privát metódusok

Gyakori kérdés az egységtesztekkel kapcsolatban, hogy a privát metódusokat hogyan teszteljük? (Most attól az apróságtól tekintsünk el, hogy valójában JavaScript-ben nincs is privát metódus. Dokumentációs kommenttel jelezhetjük a metódusunk láthatóságát, megfelelő IDE ezt a függvénylistában jelzi is nekünk.) A válasz roppant egyszerű, és rendkívül kényelmes. Nem kell tesztelni a privát elemeket. Egy jó specifikáció arról szól, hogy mit valósítsunk meg, nem pedig arról, hogy hogyan valósítsuk meg. Amíg a programunk megfelel a specifikációnak, addig a privát metódusokat nyugodtan alakíthatjuk, nem rontunk el semmit. Ennek szellemében, akkor kicsit alakítsuk át a fetch függvényünket.
...
fetch: function () {
    var me = this, re = this.variableRegexp;
    return this.html.replace(re, function (match, name) {
        return me.getValueByName(name);
    });
},
/** @private */
getValueByName: function (name) {
    return this.values[name] || '';
}
...

Ismét reguláris kifejezések

A következő lépésben zárjuk le a változókhoz tartozó reguláris kifejezés vizsgálatát, először a paraméter nélküli módosítók:
...
it('Szuros vatlozo', function () {
    var str = 'Szia {$name:upper}!',
        match = re.exec(str);
    expect(match[0]).toEqual('{$name:upper}');
    expect(match[1]).toEqual('name');
    expect(match[2]).toEqual('upper');
});
...
A hozzá tartozó megoldás:
JSTemplate.prototype = {
    variableRegexp: /\{\$(\w+)(?:\:(\w+))?\}/g,
    ...
Majd a paraméteres verzió:
...
it('Parameteres vatlozo', function () {
    var str = '{$content:truncate(10)}',
        match = re.exec(str);
    expect(match[0]).toEqual('{$content:truncate(10)}');
    expect(match[1]).toEqual('content');
    expect(match[2]).toEqual('truncate');
    expect(match[3]).toEqual('10');
});
...
A végleges reguláris kifejezés:
JSTemplate.prototype = {
    variableRegexp: /\{\$(\w+)(?:\:(\w+)(?:\((.*?)\))?)?\}/g,
    ...
Álljunk meg egy pillanatra! Azt várnánk, hogy minden szépen kizöldül, ám hibára futottunk. Azok, akik jártasak a reguláris kifejezésekben biztos tudják, hogy mi a hiba. A többieknek előbb, még egy gyors ellenőrzést ajánlok. Futassuk csak ezt a tesztet le önállóan. Azaz kattintsuk a hibás tesztre. Meglepődtünk? Ha önállóan futtatjuk a tesztet, akkor nincs hiba! A tesztektől egy fontos elvárás, hogy függetlenek legyenek egymástól, illetve ne módosítsák a környezetüket. Jelen példában ez nem áll fenn, a második teszt elrontja a harmadik tesztet. Egy gyors kiegészítés, amit minden teszt után el kell indítani:
describe('regexp1', function () {
    var template = new JSTemplate(),
        re = template.variableRegexp;

    afterEach(function () {
        re.lastIndex = 0;
    });
    ...
Ezzel a kiegészítéssel ismét az összes teszt zöldre váltott!

Módosítók

Az előző fejezet után egy könnyebb téma következik, készítsünk el néhány módosítót, amivel be tudjuk mutatni a sablon rendszerünket. Ezek a függvények tesztelés szempontjából is hálásak. Néhány egyszerű string műveletről lesz csak szó. Először jöjjön a csupa nagybetűre alakítás:
describe('filters', function () {
    var fn = JSTemplate.filters, str = 'Szia vilag!';
    it('upper', function () {
        expect(fn.upper(str)).toEqual('SZIA VILAG!');
    });
});
A megvalósítás.
JSTemplate.filters = {
    upper: function (str) {
        return String(str).toUpperCase();
    }
};
Kisbetű és a megvalósítás.
...
it('lower', function () {
    expect(fn.lower(str)).toEqual('szia vilag!');
});
...
lower: function (str) {
    return String(str).toLowerCase();
}
Végül a substr és a truncate:
...
it('substr', function () {
   expect(fn.substr(str, 5)).toEqual('vilag!');
   expect(fn.substr(str, 0, 5)).toEqual('Szia ');
});
...
substr: function (str, start, length) {
    return String(str).substr(start, length);
}
...
it('truncate', function () {
    expect(fn.truncate(str, 100)).toEqual('Szia vilag!');
    expect(fn.truncate(str, 10)).toEqual('Szia vi...');
});
...
truncate: function (str, length) {
    str = String(str);
    return str.length > length - 3 ? str.substring(0, length - 3) + '...' : str;
}

A módosítók használata

Nézzük, hol tartunk! A reguláris kifejezéssel már tudunk módosítókat és paramétereket is lekezelni, és vannak elkészített módosítók. Adja magát, hogy a következő lépés az legyen, hogy a sablonrendszer is használja fel az előbbieket:
describe('filtered', function () {
    it('Szuros valtozo', function () {
        var str = 'Szia {$name:upper}!',
            obj = {
                'name': 'vilag'
            },
            result = 'Szia VILAG!',
            t = new JSTemplate(str);
        t.append(obj);
        expect(t.fetch()).toEqual(result);
    });
});
Majd a megvalósító kód:
...
fetch: function () {
    var me = this, re = this.variableRegexp;
    return this.html.replace(re, function (match, name, filter) {
        var value = me.getValueByName(name);
        if (filter && JSTemplate.filters[filter]) {
            value = JSTemplate.filters[filter].call(me, value);
        }
        return value;
    });
},
...
Adjunk meg egy paramétert is:
describe('params', function () {
    it('Parameteres valtozo', function () {
        var str = '<div class="{$cls:lower}">{$content:truncate(20)}</div>',
            obj = {
                'cls': 'Content',
                'content': 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.'
            },
            result = '<div class="content">Lorem ipsum dolor...</div>',
            t = new JSTemplate(str);
        t.append(obj);
        expect(t.fetch()).toEqual(result);
    });
});
A hozzá tartozó kód:
...
return this.html.replace(re, function (match, name, filter, param) {
    var value = me.getValueByName(name);
    if (filter && JSTemplate.filters[filter]) {
        value = JSTemplate.filters[filter].call(me, value, param);
    }
    return value;
});
...
Több paraméter esetén már a paraméterek különválasztásával is foglalkozni kell. A teszt:
...
it('Tobbparameteres valtozo', function () {
    var str = '<p>{$content:substr(6, 5)}</p>',
        obj = {
            'content': 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.'
        },
        result = '<p>ipsum</p>',
        t = new JSTemplate(str);
    t.append(obj);
    expect(t.fetch()).toEqual(result);
});
...
Végül a kód:
...
fetch: function () {
    var me = this, re = this.variableRegexp;
    return this.html.replace(re, function (match, name, filter, param) {
        var value = me.getValueByName(name), params;
        if (filter && JSTemplate.filters[filter]) {
            params = me.createFilterParams(value, param);
            value = JSTemplate.filters[filter].apply(me, params);
        }
        return value;
    });
},
/** @private */
createFilterParams: function (value, param) {
    var result = String(param || '').split(','), i;
    for (i = 0; i < result.length; ++i) {
        result[i] = result[i].trim();
    }
    result.unshift(value);
    return result;
}
...
Továbbra is minden zöld. Itt meg lehet jegyezni, hogy talán a számmá alakítással is foglalkozhatnánk. Ha tudunk olyan tesztet készíteni, ami a jelenlegi megoldással hibára fut, akkor készítsük el! Ám ha csak egy esetleges későbbi probléma miatt módosítanánk a kódon, akkor ezt a módosítást halasszuk el. Készítsünk még egy tesztet, ahol több változóval is dolgozunk, nehogy ismét abba a hibába essünk, hogy elsőre helyes eredményt kapunk a reguláris kifejezéssel, másodszorra meg nem.
...
it('Tobb tobbparameteres valtozo', function () {
    var str = '<p>{$content:substr(6, 5)}</p><p>{$content:substr(6, 11)}</p>',
        obj = {
            'content': 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.'
        },
        result = '<p>ipsum</p><p>ipsum dolor</p>',
        t = new JSTemplate(str);
    t.append(obj);
    expect(t.fetch()).toEqual(result);
});

Foreach

Remélem, hogy már látszódik, hogy nagyon közel vagyunk a végéhez. Szükségünk van egy újabb reguláris kifejezésre, amely kiszedi a sablonunkból a foreach tag-et.
describe('regexp2', function () {
    var template = new JSTemplate(),
        re = template.foreachRegexp;

    afterEach(function(){
        re.lastIndex = 0;
    });

    it('Ciklus ellenorzes', function () {
        var str = '<ul><foreach name="$list"><li>{$content}</li></foreach></ul>',
            match = re.exec(str);
        expect(match[0]).toEqual('<foreach name="$list"><li>{$content}</li></foreach>');
        expect(match[1]).toEqual('list');
        expect(match[2]).toEqual('<li>{$content}</li>');
    });
});
Az előző reguláris kifejezés után ez már gyerekjáték.
JSTemplate.prototype = {
    ...
    foreachRegexp: /<foreach name="\$(\w+)">(.*)<\/foreach>/g,
    ...
Készítsük el a tesztet, ahol már ezt használni fogjuk:
describe('foreach', function () {
    it('Valtozo listaval 1.', function () {
        var str = '<h1>{$header}</h1><ul><foreach name="$list"><li>{$content}</li></foreach></ul>',
            obj = {
                'header': 'Lista',
                'list': [{
                    'content': '1.'
                }, {
                    'content': '2.'
                }, {
                    'content': '3.'
                }]
            },
            result = '<h1>Lista</h1><ul><li>1.</li><li>2.</li><li>3.</li></ul>',
            t = new JSTemplate(str);
        t.append(obj);
        expect(t.fetch()).toEqual(result);
    });
});
A megvalósításhoz egy apró trükkre lesz szükségünk, a foreach belsejét egy másik JSTemplate objektummal valósítjuk meg, azaz:
    ...
    fetch: function () {
        ...
        return this.doForeach().replace(re, function (match, name, filter, param) {
            ...
        });
    },
    /** @private */
    doForeach: function () {
        var me = this, re = this.foreachRegexp;
        return this.html.replace(re, function (match, name, content) {
            var list = me.getListValueByName(name), i, subTemplate, result;
            if (!list.length || !content) {
                return '';
            }
            result = [];
            subTemplate = new JSTemplate(content);
            for (i = 0; i < list.length; ++i) {
                result.push(subTemplate.append(list[i]).fetch());
            }
            return result.join('');
        });
    },
    /** @private */
    getListValueByName: function (name) {
        if (!this.values[name]) {
            return [];
        }
        return [].concat(this.values[name]);
    },
    ...
Ezzel akkor készen is lennénk, még utoljára adjuk hozzá azt a tesztet, amivel a cikk indult:
...
it('Valtozo listaval 2.', function () {
    var template = [
            '<div class="{$cls}">',
                '<foreach name="$users">',
                    '<a href="?id={$id}">{$name} ({$age})</a><br />',
                '</foreach>',
            '</div>'],
        obj = {
            'cls': 'users',
            'users': [{
                'id': 1,
                'name': 'John',
                'age': 21
            }, {
                'id': 2,
                'name': 'Bob',
                'age': 30
            }]
        },
        result = '<div class="users"><a href="?id=1">John (21)</a><br /><a href="?id=2">Bob (30)</a><br /></div>',
        t = new JSTemplate(template);
    t.append(obj);
    expect(t.fetch()).toEqual(result);
});
...
Az utoljára hozzáadott teszt is hibátlanul lefutott, elkészültünk!

Még egy kis kitérő

Ha ezt a kódot most félreteszed, majd később majdnem ismeretlenül ismét hozzányúlsz, akkor is bátran módosíthatod, mert biztos lehetsz benne, hogy nem törhetsz el működő kódot. A cikk elején szándékosan emeltem ki, hogy ne azt várjuk, hogy a módszerrel hibátlan kódot tudunk írni, hanem azt, hogy egy könnyen módosítható kódhoz jutunk. Hogy ez a gyakorlatban mit jelent? Miután az elkészült kódot elkezdjük használni, hamar megtapasztalhatjuk, hogy annak ellenére, hogy a foreach XML szerű alakban várjuk, ha két szóközt teszünk a foreach és a name közé, akkor már nem illeszkedik a mintára. Semmi gond, a specifikációnk valóban erre nem tért ki. Készítsünk egy tesztet a feladathoz:
...
it('Felesleges white-space-ek', function () {
    var str = '<foreach   name="$list" ></foreach>',
        match = re.exec(str);
    expect(match[0]).toEqual(str);
    expect(match[1]).toEqual('list');
    expect(match[2]).toEqual('');
});
...
Majd a hozzá tartozó kód:
...
foreachRegexp: /<foreach\s+name="\$(\w+)"\s*>(.*)<\/foreach>/g,
...
Futassuk gyorsan le az összes tesztet. Mindegyik teszt hibátlanul lefutott, tehát ezzel a módosítással nem rontottunk el semmit. Természetesen kisebb-nagyobb módosításokra biztos szükség lenne ahhoz, hogy ezt a kódot valós környezetben is használjuk, az Undefined résznél elég elnagyolt voltam, ennek a pontosítása megnézhető itt. További apróságok is lennének, de azokat már az olvasóra bízom. :)

Zárszó

Ezzel a példával szerettem volna kis ízelítőt adni a TDD-ben rejlő lehetőségekről. A cikk hosszabb lett, mint amilyennek először terveztem, bár több lépésnél még így is nagyobbat ugrottam, mint amekkorát fejlesztés közben tennénk. Remélem, ennek ellenére is sikerült átadnom, hogy mire jó, miért is hasznos TDD-vel fejleszteni.

Linkek