13. hét: tesztelés, refaktorálás

Czirkos Zoltán · 2019.02.27.

Heti kiegészítő feladatok

1. Tesztek

Tesztelést segítő osztály

„Próbáljuk ki, hogy működik-e!”

A manuális tesztelés sok időnket elviszi. Pedig a tesztelést nem muszáj ám kézzel csinálni. Ha adott egy függvény, amiről tudjuk hogy milyen bemenetre milyen kimenetet kell adjon... Akkor hívjuk meg, és ellenőrizzük a kapott eredményt! A várt eredményeket a kapottakkal összevethetjük; egy osztály minden funkcióját tesztelhetjük ilyen módon.

Az automatikus teszt futtatható specifikáció. Elindítjuk a tesztet, és onnantól minden további teendő nélkül megkapjuk a választ: teljesíti-e a program a specifikációját, vagy nem.

A tesztek kétfélék lehetnek:

  • A specifikáció alapján készült tesztek. Ebben a szemléletben: amire nincs teszt, az mintha nem is lenne előírva. Ha szükséges egy bizonyos funkció, tessék rá tesztet írni.

  • Az élet hozta tesztek. Hogyan lesz egy ilyen?

    1. Kiderül, hogy van egy bug a programban.
    2. Kitalálunk egy bemenetet, amivel a bug előcsalogatható.
    3. Megírjuk a tesztet, ami ezzel a bemenettel eteti a programot. Kipróbáljuk, hogy tényleg előjön-e a hiba.
    4. Kijavítjuk a programot. A javítás után a teszt is jelzi, hogy rendben van a kimenet.

Miért jó ez? Mert az a hiba soha nem fog többé előjönni; a teszteknek része lesz ez is.

Írj tesztelést segítő osztályt! Ennek legyen egy .test() metódusa, teszt_neve, elvárt_érték, kapott_érték paraméterekkel, amely

  • Kiírja, hogy sikeres a teszt, ha elvárt_érték == kapott_érték.
  • Sikertelenséget jelez, ha nem egyenlőek.
  • Közben számolja, hány teszt volt sikeres, és mennyi volt összesen.
Test t1;
t1.test("A gép tud osztani.", 2, 10/5);
t1.test("Én tudok szorozni.", 11, 3*4);
t1.report();

OK    A gép tud osztani.
FAIL  Én meg tudok szorozni. 11 != 12

1 sikeres teszt, 2 összesen.

Használd ezt a következő feladatban!

A maximumkeresés

Adott az alábbi, elfuserált maximumkereső függvény:

#include <iostream>

int max(double *tomb, size_t meret) {
    double m = tomb[1];
    for (size_t i = 1; i < meret-1; ++i)
        if (tomb[i] > m)
            m = tomb[i];
    return m;
}

int main() {
    double szamok[5] = { 6, 8, 13.1, 8.7, 4 };
    std::cout << max(szamok, 5);
}

Mi a legszembetűnőbb hibajelenség, ami futtatás után is látszik? Mielőtt kijavítanád a hibát, írj rá tesztet! Előbb lásd, hogy a teszt jelzi, probléma van, és a hibát utána javítsd csak ki!

A függvényben két további hiba is el van rejtve. Találj ki olyan bemeneteket, amelyek esetén jelentkeznek a problémák, írd meg a teszteket, végül pedig javítsd a hibákat!

2. AlakzatProxy

Bevezető

Itt egy „dinamikus sztringek dinamikus tömbje” osztály. Ebben két ** van: az egyik azért, mert dinamikus tömböt tárol, a másik pedig azért, mert a dinamikus tömbben dinamikus sztringek vannak.
class SztringTomb {
    char **data;
    /* ... */
};

Ennek az osztálynak másoló konstruktort, destruktort írni szörnyűséges feladat.

Itt egy másik „dinamikus sztringek dinamikus tömbje” osztály:

class SztringTomb {
    std::vector<std::string> data;
    /* ... */
};

Az egyik * a vektor osztályba került, a másik * a sztringbe. Mivel a vektor és a sztring is rendelkezik saját másoló konstruktorral és destruktorral (mindkettő érték szerint kezelhető), ezért nekünk semelyiket sem kell megírni.

A probléma

Itt egy heterogén kollekció:

class Alakzat {
    public:
        virtual void kiir() const = 0;
        virtual ~Alakzat() {}
};

class Tegla : public Alakzat {
    public:
        virtual void kiir() const {
            std::cout << "Egy egy teglalap vagyok.\n";
        }
};

class Kor : public Alakzat {
    public:
        virtual void kiir() const {
            std::cout << "En egy kor vagyok.\n";
        }
};

class Rajztabla {
    private:
        Alakzat **a;
    public:
        /* ... */
};

Ebben megint két *-ot látunk. Az egyik * a dinamikus tömb miatt van, a másik * pedig a heterogén típusok miatt. A tömb miatti *-gal már tudjuk, mit kezdjünk:

class Rajztabla {
    private:
        std::vector<Alakzat*> a;
    public:
        /* ... */
};

Miért baj a másik *? Azért, mert emiatt még mindig destruktort kell írnunk a rajztáblának. És azért, mert hiába másoljuk le a pointereket, új alakzatok nem keletkeznek (bekavar az indirekció) – az új rajztábla a régi alakzatait használja, azokra hivatkozik csak.

Most ezt a *-ot fogjuk eltüntetni: másik osztályba kiszervezni, így refaktorálni a kódot.

A feladatok

Először:

  1. Írj a rajztáblának hozzaad(Alakzat *) és listaz() függvényt!

  2. Írj rövid teszt kódot, amelyben hozzáadsz a rajztáblához néhány dinamikusan foglalt alakzat.

  3. Írd meg a rajztábla destruktorát és másoló konstruktorát, hogy lásd, pontosan mi a probléma. Vedd észre, hogy itt a heterogén kollekció másolása miatt szükség van az alakzatok klónozására; vezesd be az ehhez szükséges klonoz() függvényt az osztályhierarchiába!

  4. Teszteld a másoló konstruktort, hogy meggyőződj róla: helyes a másolás, tényleg új alakzatok keletkeznek! Ezt leginkább úgy fogod látni, hogy a destruktor nem száll el (mert nem akarja kétszer felszabadítani ugyanazt az alakzat).

És most jön a lényeg. Az ötlet az, hogy létrehozol egy AlakzatProxy osztályt. Ez az osztály egyetlen egy darab alakzat fog tartalmazni (amely persze heterogén lehet, mert lehet téglalap, kör vagy bármi más). Az AlakzatProxy objektum érték szerint kezelhető. Ha a proxy másolódik, a benne lévő alakzat is másolódik. Ha a proxy megszűnik, a benne lévő alakzat is megszűnik. Ezen felül pedig, duplikálja az alakzat interfészét: mindent, amit az alakzattal lehet csinálni (kiírni, kerületét kiszámítani stb. – most csak a kiírás van), azt a proxyval is lehet; minden függvényhívást a tárolt alakzatnak továbbít, helyettesíti azt.

AlakzatProxy a1(new Kor);
{
    AlakzatProxy a2 = a1;       // a kör másolata
    a2.kiir();                  // Én egy kör vagyok
                                // a2 megszűnik, másolt kör megszűnik
}

Tehát másodszor:

  1. Implementáld a fenti minta alapján az AlakzatProxy osztályt.
  2. Dolgozd át a rajztábla osztályt úgy, hogy a benne lévő Alakzat*-ot AlakzatProxy-ra cseréled.
  3. Vizsgáld meg a rajztábla előbb megírt másoló konstruktorát és destruktorát. Hogyan kell módosítani őket? Miért?

Extrák

  1. Zavarhat, hogy az Alakzat osztály interfészét az AlakzatProxy osztály interfészén duplikálni kellett. Töröld ki azt onnan, és írj helyette operator* (a tárolt alakzat referenciáját visszaadó), és operator-> (a tárolt alakzatra mutató pointert visszaadó) függvényt! Így ezekkel bármelyik függvény elérhetővé válik:
AlakzatProxy a1(new Kor);
rajzol(*a1);        // void rajzol(Alakzat&)
a1->kiir();         // Alakzat::kiir()
  1. Rejtsd el a dinamikus memóriakezelést az AlakzatProxy osztályba egyszer s mindenkorra! Írj az AlakzatProxy osztálynak konstruktort, amelyik tetszőleges paraméterként kapott Alakzat objektumról dinamikus másolatot készít, és azt tárolja!
AlakzatProxy a1(Kor());
AlakzatProxy a2(Tegla());

a1->kiir();          // En egy kor vagyok.
a2->kiir();          // En egy teglalap vagyok.

Vedd észre, hogy ismeretlen típusú alakzatról dinamikus másolatot készítő függvényed már van.