Unit test on minu sõber! Miks?

17.02.2007  |  Gunnar

Unit testid – mis need on ja mis kasu neist on? Räägin omast väikesest kogemusest, kuidas tulemused hakkasid sündima kohe ja kui palju igasugust tüütut peavalu ära jäi. Muideks, seda juttu võiksid lugeda ka IT-juhid, kes ise küll arendusega ei tegele, kuid vastutavad arendustööde tellimise eest.

Niisiis, asja juurde. Unit testid kujutavad endast erinevate tarkvara osade teste, mis aitavad veenduda testitavate osade töökindluses. Tegemist pole mitte testidega, mis teeksid automaatselt ära inimtestija töö, vaid testidega, mis jäävad programmi koodi tehnilise ja inimtestija töö vahepeale. Et selgem oleks, siis võtame käsile ühe reaalelulise näite.

Hakkasin hiljuti kirjutama DT projektide träkkimise süsteemi uut versiooni. Vanemal on palju puudusi ja keerukaid kohti, mida ma uues versioonis soovin vältida. Alustasin business layer'i klasside loomisest. Enne viisin loomulikult läbi analüüsi ja modelleerimise. Unit testide loomise võtsin käsile kohe alguses ja juba esimesest tunnist alates olin ikka täiega võidumees.

Töökeskkond

Esiteks siis töökeskkonnast, mida kasutan. Arendus toimub Visual Studio .Net 2005 Professional peal. Et süsteem oleks paindlikum ning äriloogika kasutatav mitmel pool mujal, siis äriloogika jaoks tegin eraldi solution'i, kus on kaks Class Library tüüpi projekti. Üks siis äriloogika ja teine selle testide jaoks.

Unit testide tegemiseks võtsin kasutusele sellise tarkvara nagu MbUnit. Et teste oleks mugavam Visual Studio keskkonnas käivitada, siis installeerisin ära ka sellise asja nagu TestDriven.NET. Võtsin tasuta versiooni, sest enne kui ostan, tahan proovida, et see jubin ikka töötab ka.

Testide projektile tuleb lisada kaks reference'i:

  • reference teegile, kus on testitavad klassid,
  • reference MbUnit'i teegile MbUnit.Framework.dll.

Nüüd peaksime olema nii kaugel, et saame liikuda järgmise sammuni, milleks on testitava klassi loomine.

Kirjutame testimiseks klassi

Vaatame näiteks klassi nimega PersonName, mis on mõeldud isikute nimede ajaloo säilitamiseks. Miks selline lähenemine ja miks selline klass, sellest teen juttu edaspidi. Hetkel aga lepime sellega, et on taoline klass. Ja veel üks asi. Siintoodu on ainult fragment PersonName klassist. Seega ärge selle klassi enda olemusse ja sügavamasse mõttesse hetkel laskuge.

using System;

namespace DT
{
    public class PersonName
    {
        private Person oPerson = null;
        private string sFirstName = null;
        private string sMiddleName = null;
        private string sLastName = null;

        public PersonName(Person NameOwner, string FirstName,
                        string LastName, DateTime ValidFrom)
        {
            this.Person = NameOwner;
            this.FirstName = FirstName;
            this.LastName = LastName;
            this.ValidFrom = ValidFrom;
        }
        public string FirstName
        {
            get { return this.sFirstName; }
            set
            {
                String sErr;
                if (value == null)
                {
                    sErr = "Eesnimi ei saa olla null!";
                    throw new Exception(sErr);
                }
                value = value.Trim();
                if (value == String.Empty)
                {
                    sErr = "Eesnimi ei saa olla tühi string!";
                    throw new Exception(sErr);
                }
                if (value.Length> 25)
                {
                    sErr = ""Eesnimi on liiga pikk!"";
                    throw new Exception(sErr);
                }
                this.sFirstName = value;
            }
        }
        public string LastName
        {
            get { return this.sLastName; }
            set
            {
                String sErr;
                if (value == null)
                {
                    sErr = "Perekonnanimi ei saa olla null!";
                    throw new Exception(sErr);
                }
                value = value.Trim();
                if (value == String.Empty)
                {
                    sErr = "Perekonnanimi ei saa olla tühi string!";
                    throw new Exception(sErr);
                }
                if (value.Length> 25)
                {
                    sErr = "Perekonnanimi on liiga pikk!";
                    throw new Exception(sErr);
                }
                this.sLastName = value;
            }
        }
        public string MiddleName
        {
            get { return this.sMiddleName; }
            set
            {
                String sErr;
                if (value != null)
                    value = value.Trim();
                if (value == String.Empty)
                    value = null;
                if (value != null)
                    if (value.Length> 25)
                    {
                        sErr = "Keskmine nimi on liiga pikk!";
                        throw new Exception(sErr);
                    }
                this.sMiddleName = value;
            }
        }
        public Person Person
        {
            get { return this.oPerson; }
            set
            {
                String sErr;
                if (value == null)
                {
                    sErr = "Nime omanik ei saa olla null!";
                    throw new Exception(sErr);
                }
                this.oPerson = value;
            }
        }
    }
}

Klassi dokumentatsioon

Lühidalt võiksime klassi dokumenteerida ära selliselt:

  • PersonName – klassi konstruktor.
    Argumendid:

    • NameOwner – isik, kellele nimi kuulub.
    • FirstName – isiku eesnimi.
    • LastName – isiku perekonnanimi.
  • FirstName – määrab või tagastab isiku eesnime (null ja tühi string väärtustena keelatud, maksimaalne pikkus 25 tähemärki).
  • MiddleName – määrab või tagastab isiku keskmise nime (tühi väärtus teisendatakse nulliks, maksimaalne pikkus 25 tähemärki).
  • LastName – määrab või tagastab isiku perekonnanime (null ja tühi string väärtustena keelatud, maksimaalne pikkus 25 tähemärki).
  • Person – määrab või tagastab isiku, kellele nimi kuulub (null on keelatud).

Klassi konstruktor garanteerib selle, et ei saaks luua vigaste väärtustega objekti. Kõik kohustuslikud väärtused antakse klassile kohe ette. Märgin siinkohal ära, et ilma argumentideta konstruktorit lubame kasutada projektisiseselt ainult O/R mapper'il.

Mida peame testima?

Klassi lühidast dokumentatsioonist näeme, et klassile on kehtestatud mitmeid ärireegleid. Näiteks kohustuslikud omadused ei tohi omada väärtust null. Tekstilistel väärtustel on olemas maksimaalne lubatud pikkus, mittekohustuslik keskmine nimi peab aga tühja väärtuse korral andma tulemuseks nulli.

Kõiki neid reegleid saame me kontrollida unit testide abil. Testid peame kirjutama ainult ühe korra, edaspidi peame enne muudatuste andmist testija kätte unit testid käima laskma ning veenduma, et vigu ei tekkinud. Muideks, see võtab aega paar sekundit kuni mõni minut sõltuvalt klasside ja testide arvust.

Nüüd kui meil on klass koos ärireeglitega olemas võime asuda testide kirjutamise juurde.

Unit testide loomine

Klassi PersonName testimiseks loome testide teeki klassi nimega PersonNameTest. Et nimede testid on suhteliselt sarnased, erinedes peamiselt meetodi nimede poolest, siis toon siinkohal ära nimedest ainult eesnime testimise.

Klass koos Person omaduse omistamise testiga on selline

using DT;
using MbUnit.Core.Framework;
using MbUnit.Framework;
using System;

namespace DTLibTest
{
    [TestFixture("PersonName klassi test")]
    public class PersonNameTest
    {
        [Test("Isik pole määratud")]
        [ExpectedException(typeof(Exception))]
        public void PersonIsNull()
        {
            DateTime tValidFrom = DateTime.MinValue;
            PersonName pn = new PersonName(null, null, null, tValidFrom);
        }
    }
}

TestFixture klassi ees ütleb, et tegemist on klassiga, mis sisaldab teste. Test meetodi nime ees ütleb, et see meetod viib läbi mingisuguse testi. Need väärtused on olulised unit testide süsteemi jaoks, mida me kasutame.

Test: Eesnimi ei tohi olla null

Eesnime korral keelasime ära väärtuse null, sest eesnimi on kohustuslik omadus PersonName objektil. Tegemist on ärireegliga ja järelikult peame me testima selle kehtivust.

[Test("Eesnimi pole määratud")]
[ExpectedException(typeof(Exception))]
public void FirstNameIsNull()
{
    Person p = new Person();
    DateTime tValidFrom = DateTime.Now.AddDays(-2);
    PersonName pn = new PersonName(p, null, null, tValidFrom);
}

Test: Eesnimi ei tohi olla tühi string

Eesnime korral keelasime ära ka tühja väärtuse, sest seegi tähendab meie jaoks, et eesnime pole antud.

[Test("Eesnimi on tühi string")]
[ExpectedException(typeof(Exception))]
public void FirstNameIsEmpty()
{
    Person p = new Person();
    DateTime tValidFrom = DateTime.MinValue;
    PersonName pn = new PersonName(p, String.Empty, null, tValidFrom);
}

Test: Eesnimi on liiga pikk

Eesnime maksimaalseks pikkuseks kehtestasime eespool 25 tähemärki.

[Test("Eesnimi on lubatust pikem")]
[ExpectedException(typeof(Exception))]
public void FirstNameIsTooLong()
{
    Person p = new Person();
    DateTime tValidFrom = DateTime.MinValue;
    String s = "1234567890";
    s += s;
    s += s;
    PersonName pn = new PersonName(p, s, null, tValidFrom);
}

Test: Eesnime trimmimine

Kui eesnimi sisaldab ees või järel tühikuid, siis peame need automaatselt eemaldama. Kontrollime, kas see on nii.

[Test("Eesnime trimmimine")]
public void FirstNameTrim()
{
    Person p = new Person();
    DateTime tValidFrom = DateTime.MinValue;
    String s = "Jaan  ";
    PersonName pn = new PersonName(p, s, "Test", tValidFrom);
    Assert.AreEqual(s.Trim(), pn.FirstName, "Eesnime trim ebaõnnestus");
}

Test: Eesnimi on korrektne

Kõige lõpuks peame testima seda, kas kõikide tingimuste täitmisel on klass nõus korrektse eesnime vastu võtma või ei. See test on vajalik selleks, et leida üles vigased kontrolltingimused.

[Test("Eesnimi on korrektne")]
public void FirstNameIsCorrect()
{
    Person p = new Person();
    DateTime tValidFrom = DateTime.MinValue;
    String s = "Jaan";
    PersonName pn = new PersonName(p, s, "Test", tValidFrom);
    Assert.AreEqual(s, pn.FirstName, "Eesnime omistamine ebaõnnestus");
}

Testime koodi

Nüüd on meil PersonName klassi testimiseks vajalikud testid olemas ning võimegi asuda testimise juurde. Enne testimist peame kompileerima nii äriloogika kui ka testide teegi. Testi käivitamiseks vajutan ma testide projekti peal paremat hiirenuppu ja valin menüüst Test With => MbUnit.

See käivitab MbUnit'i graafilise kasutusliidese, mille kaudu saan ma testid mugavalt käivitada ning vajadusel võin tulemused lasta välja näiteks HTML-formaadis. See võimaldaks mul testide tulemused panna üles näiteks intranetti, kus programmeerijad saavad probleemsete testidega tutvuda.

Et mul on PersonName klassi jaoks teste hulka rohkem, kui siin välja tõin, siis näeb mul MbUnit'i liides enne testi välja selline:

Nupp Run käivitab testid. Antud juhul on siis leitud 20 testi, mis käivitada ning ma lasen käima need kõik. Kui testid õnnestuvad, siis on ikoonid rohelised. Kui mõni ebaõnnestub, siis viib selleni punaste ikoonide rivi. Minul õnnestusid seekord kõik testid. Ja nagu alltoodud pildilt näeme, võtsid testid aega pool sekundit. Pole paha!

Kaua mul aega kulus?

Võtame nüüd siis otsad kokku tasapisi ning hindame ära tegevusest saadud kasu ning anname hinnangu tegevusele. Testide tegemiseks vajaliku keskkonna seadistamine oli mõne minuti töö. Esimesel korral võttis see aega umbes 20 minutit. Et teinekord juba oskajam olen, siis taandub keskkonna seadistamine küsimus tõenäoliselt paarile minutile.

Testide osas leidsin info internetist üles küllaltki kiiresti. Materjale on palju ning kes vähegi oskajam otsija on, leiab kindlasti kõik vajaliku info kähku üles.

Testide kirjutamisele kulutasin aega kokku umbes pool tundi. Arvestades, et tegemist on küllaltki lihtsate testidega, siis keerukamatel juhtudel kulub aega tõenäoliselt rohkem. Samas on need testid vaja kirjutada ainult ühe korra ning näiteks äriloogika reeglite muutumisel peame vastavad testid lihtsalt muutustega sünkrooni viima. Seega pole testi koostamisele kuluv aeg midagi sellist, mis projektide ajakavasse ei mahuks.

Millega ma kohe kasumisse läksin?

Testide koostamisega läksin ma kasumisse koheselt. Peale esimest käivitamist leidsin äriloogika reeglite täitmise osas paar väikest viga. Ma sain need vead kätte paari sekundiga. Täpselt nii palju kulub aega testide käivitamiseks. MbUnit'i kasutusliides andis mulle ka operatiivset infot selle kohta, mis probleemid tekkisid. Käsitsi testimine oleks aega võtnud minuteid ning seega oli mu ajaline kokkuhoid võrreldes tavalisega oma häid sadu kordi.

Palju ma aega oleks kokku hoidnud meeskonnatöö korral?

Minu hinnangul oleks meeskonnatöö korral ajaline kokkuhoid olnud tuhandetes kordades. Esiteks oleks viga tulnud välja alles testija käes ja see oleks tähendanud ühte mõttetut testimise tsüklit. Testija oleks kulutanud oma aega ning oleks mulle tagasi saatnud aruande, kus on kirjas kaks viga.

Et testija kätte läheb kood, mis on juba otsapidi koodihoidlasse jõudnud, siis oleks minu tehtud viga jõudnud ka teisteni, kes samas projektis programmi kirjutavad. Võib-olla oleks olnud neist mõne töö mingiks ajaks häiritud. Ja ma ise oleks pidanud oma aega kulutama sellele, et selgitada teistele vea olemust ning rahustada kõik maha – kohe parandan ja on korras.

Tühi testimise tsükkel jäi ära, kellegi tööd ma ei häirinud ning kellelegi midagi selgitama ei pidanud. Ma sain probleemid kätte poole sekundiga ning parandustele kulus aega paar minutit. Lisaks sellele saan ma olla kindel, et kui ma antud koodi koodihoidlasse postitan, siis ei tekita see probleeme teistele projektis osalejatele, kelle masinasse minu tehtud muudatused jõuavad.

Ajalist kokkuhoidu võime hinnata isegi suuremaks. Nimelt, kui testija poleks minu viga leidnud ning see oleks jõudnud tellija kätte testimisele, siis oleks veaaruanne tulnud tellijalt. Vahepeal oleks aga tekkinud ajakulu ka uue versiooni paigaldamise näol testserverisse. Ja veel enam tekkiks ajalist kokkuhoidu siis, kui ka tellija poleks viga märganud ning oleks selle tagasiside saanud oma klientidelt või töötajatelt. Sel juhul peaksid mõlemad pooled tegema peale vigade parandamist läbi uue testimise ja paigaldamise tsükli. Antud juhul said need ajakulud kenasti välditud.

Kokkuvõtteks

Kokkuvõtteks võin öelda, et unit testide kasutamine tõstis mu produktiivsust ning efektiivsust tohutult. Esiteks hoidis testide kirjutamisesse tehtud lühike ajaline investeering kokku aega, mis kulub vigade leidmisele. Teiseks sain ma kätte vead hetkega. Lõppkokkuvõttes sain ma palju lühema ajaga palju enam tehtud.

Kommenteeri

sulge
Saada link e-postiga

© DT 2012 | Creative Commons Attribution-Noncommercial 3.0 License | WordPress