Třída CVector – Test Driven Development


Realizujte pro třídu CVector vlastnosti podle následujících bodů společně s testy TDD.
1) Naprogramujte zjišťování počtu celkově vzniklých a aktuálně existujících prvků datového typu CVector. Počítání proveďte pomocí statických proměnných iActual a iTotal. Pro zjišťování jejích hodnot napište statické metody Aktual a Total. Každému objektu přiřaďte jednoznačný identifikátor iID, odvozený od pořadí jeho vzniku. Pro jeho zjištění implementujte metodu ID.
2) Napište metodu
Clone, která vytvoří a pomocí návratové hodnoty vrátí přesnou kopii sebe sama (tj. vrátí kopii prvku předaného pomocí this). (DU)

Kromě testovacích volání doplňte kód do vzorové funkce main.
cout << "Od začátku programu bylo vytvořeno " << CVector::Total << " objektů třídy CVector " << endl;
cout << "V současné době existuje " << CVector::Actual << " objektů třídy CVector " << endl;
cout << "Objekt a má ID " << a.ID() << endl;
a = b.Clone();


Ad zadání 1)

1) Vyzkoušejte si princip testování zdrojového kódu (unit testing) – postup návrhu, [wikipedia.org]. Postup nastavení testů TDD ve VS 2017 .

2) Pro zkoušení použijte statické metody Total a Actual. Metoda Total bude vracet celkový počet prvků třídy CVector vytvořených během programu. Metoda Actual bude vracet počet aktuálně existujících prvků třídy CVector.
Obě metody musí fungovat i v okamžicích, kdy nebude vytvořen žádný objekt CVector (proto jsou statické).
Pro korektní činnost musí každý konstruktor (vzniká objekt) připočítat jedničku k oběma proměnným (iTotal, iActual), zatímco destruktor (zánik objektu) odečte jedničku pouze od proměnné iActual.

Pozn. Řešení přidejte do řešení z minulého cvičení.


Řešení:

0) Vytvořte kopii příkladu z minulého cvičení, nebo si jeho stav zaarchivujte.

1) Pro testování je nutné vytvořit nový projekt (graficky zde). File/new/Project/Test/Native Unit Test Project. Důležitá je volba "Add to solution" protože test pracuje se zdrojovými texty jiného/stávajícího projektu.

2) Je nutné určit vazbu na testovaný projekt. V Solution Exploreru pravé tlačítko na testovaném projektu a Add / reference a zvolit příslušný projekt.

3) V Solution exploreru se vytvořil projekt pro test a v něm se vytvořil soubor pro kód testů (unittest1.cpp - nebyl-li přejmenován). Prohlédněte si tento soubor a vložte do něj (include) hlavičkové soubory, ve kterých jsou testované prvky (třídy, metody, funkce). Jelikož jsou hlavičkové soubory v jiném projektu, tj v jiném ("sousedním") adresáři, musí to respektovat cesta "../../MujProjekt/include/Funkce.h". Pro nalezení hlavičkového souboru je tedy nutné "se vrátit" v adresářové struktuře pomocí "..\" a následně se dostat do projektu s testy. (například #include "..\cv5_trida1\vector.h").

Dále musíme zajistit dosažení vlastního kódu (tj. obj souborů v druhém adresáři). V Properties/Linker/Input/Additional Dependencies je potřeba dopsat obj soubory např. check.obj; Vector.obj. To jsou obj soubory, ve kterých jsou testované metody (metody se přeloží v původním projektu do obj a v testech se použije stejný obj soubor). A nastavit cestu, kde tyto soubory jsou do properties /VC++directories /library directories: $(SolutionDir)cv5_trida1\$(IntDir); Je také možné nastavit adresář projektu jako adresář pro načítání hlaviček v testovacím projektu a includovat pouze s názvy souborů. U include se cesta nastaví: Include Directories $(SolutionDir)cv5_trida1;
Po nastavení je vhodné dát clean project a rebuild.

4) Pro test je nutné nastavit použitou architekturu procesoru (stejnou jako mají oba projekty: x64, x86 ...). Test/Test settings/Default processor architecture/x64.

Pozor - test u VS musí mít nastaven stejné parametry (x64, x86) jako byly překládány testované moduly (nastavuje se na dvou místech - překlad v menu a Test/settings/Processor Architecture )
5) Pro kontrolu výsledků testů si otevřeme okno s testy "Test/Windows/Test Explorer". Projekt přeložíme (build, rebuild) a v okně testů se objeví modrá značka s názvem třídy testu. Rozbalením se zobrazí jednotlivé testy. Testy se spustí pomocí volby Run All v Test Exploreru. Je možné nastavit i automatické spouštění testů po překladu. Názvy metod se objeví v Test Exploreru.
Výsledky se objeví v okně u jmen testů po spuštění Run All - červené skončily chybou, zelené proběhly v pořádku. Při kliknutí na test se v dolní části okna TestExploreru objeví podrobnosti. Testy je možné spustit naráz (Run All), ve skupině (Run, Run Selected), po jednom, nebo trasovat (Debug selected tests). Debug, který respektuje breakpointy je možné spusti pro danou metodu kliknutím na ikonku u jména testu ve zdrojovém souboru.
6) Automaticky generovaný název metody testu může být zmměněn - TEST_METHOD(PrvniTest) - v Test Exploreru se bude objevovat pod jmenem PrvniTest. Vlastní výsledky testu (zelená/červená) jsou dány výsledky vložených testovacích kódů.
Případná další testovací metoda se opět označí/nadefinuje pomocí TEST_METHOD(TestMethodXX). V jejím těle proběhne test. Vrací void. Výsledek testu se oznamuje pomocí konstrukce Assert::Vlasnost.

Např. vyzkoušejte v těle testu kód Assert::IsFalse(true), který jako výsledek ukáže, že test neprošel (bude červený) a následně pomocí změny na Assert::IsFalse(false) kdy by měl být test úspěšný (zelený).

Jako Vlastnost může být použito:
- AreEqual(p1,p2); - obsahově/hodnotově stejné
- AreNotEqual(p1,p2);
- AreNotSame(p1,p2); - fyzicky stejné
- IsNull(p1); IsNotNull(p1) testy ukazatelů

- IsTrue(p1) ; IsFalse(p1) - testy pro logické podmínky nebo návratové hodnoty
- Fail - násilně způsobí, že test ohlásí fail

Všechny uvedené testy mají jako další parametr zprávu, která se zobrazí v dolním okně TestExploreru v případě, že se test nepovede, při pokliknutí na test. Zápis v kódu: Assert::IsFalse(true,"Testovani neprovedeneho testu").
Dále je možné přidat číslo řádku, na kterém je test v kódu umístěn. Assert::IsFalse(true,"Testovani neprovedeneho testu", LINE_INFO()).
Test nakonec nastavte tak aby se povedl.

Pro lepší orientaci si do Output okna můžeme i vypisovat zprávy (proměnné do nich je nutné dostat tiskem do řetězce, který se zobrazí) Logger::WriteMessage(const wchar_t *message) Logger::WriteMessage(const char *message)

std::strstream str;
str << "Hodnota je " << i << '\0';
Logger::WriteMessage(str.str());

Pozn.: TEST_CLASS(jmeno) označí/nadefinuje testovací třídu a uvede její jméno

Je možné použít i makra pro spuštění před každým vytvořením třídy TEST_CLASS_INITIALIZE(method). Makra pro spuštění po každém vytvoření třídy TEST_CLASS_CLEANUP(method).


Pozn. 1: testování počtu objektů se týká konstruktorů (zvyšování počtu) a destruktoru (snižování počtu). Proto testujeme kontrutkory a destruktor. V normální situaci by bylo možné tyto testy přidat do testování při tvorbě jednotlivých konstruktorů – jelikož tyto testy (zatím) nemáme, budeme testy tvořit samostatně (bez testů vlastní inicializační činnosti konstruktorů – tj. bez kontroly správného naplnění hodnotami).
Pozn. 2: Bylo by možné otestovat všechny konstruktory v jednom testu (v jedné testovací metodě). To ovšem není nejlepší, protože test končí na první nesplněné podmínce (chybě) a proto by nedošlo ke kompletnímu testu, ale pouze k nalezení první chyby. Proto musíme při detailním testování napsat pro každý konstruktor vlastní testovací metodu.
Pozn. 3: Při více testech v jedné testovací metodě je dobré promyslet jejich řazení (tj. pořadí v jakém se budou testy provádět – což vlastně značí prioritu hlášení chyb).
Pozn. 4: Při testování můžeme testovat jednotlivé konstruktory (jak bylo popsáno výše), nebo „scénáře“ - tj. určitý postup, sekvenci, která bude složitějším kódem (v našem případě bychom mohli zkusit, zda dojde ke správnému výsledku pro kód s větším množtvím různých konstruktorů, včetně dynamického tvoření proměnných a polí, volání konstruktorů při volání metod ...).
Pozn. 5: V testu můžeme kontrolovat nárůst o správný počet prvků – ale pouze v případě, že zabráníme paralelnímu volání jednotlivých testů (v tom případě, by ostatní testy ovlivňovaly počet prvků a museli bychom předpokládat, že nárůst může být větší či menší než odpovídá kódu testu). Při paralelním volání testů mohou nastat i situace, kdy se celkový příspěvek ostatních testů projeví „opačným“ směrem než čekáme a výrazným způsobem ovlivní test. Proto při tomto testování není vhodné spouštět testy paralelně.

Pozn. 6: TDD. Test Driven Development. Tento způsob psaní programů spočívá v současné tvorbě metod/funkcí a jejich testů.

Při psaní testů se uplatňují některé postupy:

a) Arrange/Act/Assert - přichystat data pro testovanou metodu/s parametry spustit metodu/Vyhodnotit výsledky s pomocí Assert::

b) red/green/refactor (vylepši) - postupuje se (v sekci Act/Assert) po jednotlivých krocích. Promyslíme si jak by měla funkce fungovat, napíšeme test a snažíme se ho splnit. V prvním kroku by test neměl být splněn (není napsán kód) a tedy odpověď testu je "červená". Následně napíšeme kód díky kterému test projde - zelená. Následně se snažíme kód odladit (vylepšit) tak aby byl rychlejší, přehlednější ... a test (i ostatní) zůstal "zelený"


7) Založme nový prázdný test (zkopírujme předchozí test TEST_METHOD(){...} (do místa za tento test)) a pojmenujme ho TestStaticImplicit pro kontrolu přičítání v implicitním konstruktoru.

Po spuštění Run All by se měl objevit v Test Exploreru.

8) Připravíme test (Pouze prototypy testovaných funkcí a metod bez testovaného kódu):
- v testovací metodě vytvoříme vnořený blok, kde vyvoláme implicitní konstruktor (na konci bloku se automaticky zavolá destruktor). (Arrange)
- Ve významných místech (Před blokem, v bloku za konstruktorem a za blokem) ověříme počet celkových a aktuálních prvků (Act-volání konstruktorů, destruktoru a statických metod). K tomu musíme napsat statické metody iTotal, iActual (zatím s prázdnými tělíčky), které tedy zkoušíme společně s konstruktory/destruktorem.

- Ověřujte i přidělování unikátního indexu (ve většině případů by měl být větší než minulý index a o jednu menší než celkový počet prvků).

- Nakonec použijeme vhodný Assert:: pro zhodnocení činnosti (Assert). Při správné činnosti, budou před blokem počty prvků nulové (Pokud tento test nebude prvním testem, bude již Total nabývat při vstupu hodnoty různé od nuly). Za konstruktorem by měly být počty větší o jedničku. Za koncem bloku by měl být počet aktuálních prvků nula a počet vzniklých prvků jedna.

- Jelikož se implicitní kosntruktor volá i pro pole, můžeme test doplnit o další blok, kde otestujeme tvorbu pole. Stávající blok testu tedy zkopírujeme (do stejné metody testu) a místo proměnné tentokrát vytvořit pole (například 100 prvků). A opět testovat.

9) Spustíme test pro napsanou testovací metody (red/green fáze) – výsledek by měl být „červená“. To znamená, že test by neměl být úspěšný.

Nyní se snažíme (převážně na straně kódu statických metod, konstruktorů a destruktoru, ale můžeme upravit i testy) dosáhnout stavu „green“, kdy všechny testy projdou – nebo-li doplníme kód tak aby metody správně fungovaly.

10) Nakonec se zamyslíme, zda by kód testů nebo psaných metod nešel zlepšit (fáze refactor).

11) Napište test pro kopykonstruktor. Jelikož kopykonstruktor potřebuje ke své činnosti již vytvořený prvek, budeme ho testovat společně s jiným konstruktorem (implicitní konstruktor není vhodný, protože by bylo potřebné ověřit i složitější kopie). Zvolíme konverzní konstruktor s jedním prvkem. Tento test tedy bude společný pro dvě metody/konstruktory. V opačném případě bychom zde prováděli duplicitní testy konverzního konstruktoru.
Otestován by měl být kopykonstruktor prázdného prvku, i prvku inicializovaného.

Pozn.: V tomto bodě si můžete zkusit testování přidělování paměti pomocí metod knihovny check. Část kódu níže

(dále DU)

12) Napište další testy pro konstruktor z řetězce, …

13) Napište test, ve kterém zavoláte všechny konstruktory (test, že se vzájemně neovlivňují).




Příklad použití knihovny check pro testy manipulace s pamětí.

size_t allocsize_before = get_alloc_actual_size(); // mnozstvi pameti pred cinnosti

...

allocsize_after = get_alloc_actual_size(); // mnozstvi pameti po cinnosti

Assert::IsTrue(allocsize_before < allocsize_after, L"No memory allocation", LINE_INFO());

Assert::IsTrue(allocsize_before == allocsize_after, L"Memory allocated", LINE_INFO());



size_t alloc_count_before = get_alloc_count(); // pocet alokaci pred

size_t dealloc_count_before = get_dealloc_count(); // pocet uvolneni pameti pred

size_t realloc_count_before = get_realloc_count(); // pocet realokaci pred

...

size_t alloc_count_after = get_alloc_count(); // a pocty po cinnosti

size_t dealloc_count_after = get_dealloc_count();

size_t realloc_count_after = get_realloc_count();

Assert::AreEqual(alloc_count_before, alloc_count_after, L" allocate", LINE_INFO());

Assert::AreEqual(dealloc_count_before, dealloc_count_after, L" deallocate", LINE_INFO());

Assert::AreEqual(realloc_count_before, realloc_count_after, L" reallocate", LINE_INFO());





Literatura:

Vytvoření testu ve VS https://support.apple.com/en-gb/HT201263

https://docs.microsoft.com/en-us/visualstudio/test/writing-unit-tests-for-c-cpp?view=vs-2017

Příklad https://docs.microsoft.com/en-us/visualstudio/test/microsoft-visualstudio-testtools-cppunittestframework-api-reference?view=vs-2017#test_methods


DU:

Ad zadání 2)



Zadání stručně

Vyzkoušejte si princip testování zdrojového kódu (unit testing) pro metodu Clone, která vytvoří a pomocí návratové hodnoty vrátí přesnou kopii sebe sama (tj. prvku předaného pomocí this, který bude mít stejné hodnoty (ukazatele na vlastní paměť se budou lišit))



Řešení:

0) Vytvořte kopii příkladu z minulého cvičení, nebo si jeho stav zaarchivujte.

1) Pro testování vytvořte nový projekt s unit testy (jako součást stávajícího projektu). Nastavte vše potřebné a zkuste, že lze implicitní prázdný test přeložit a spustit.

2) Je nutné určit vazbu na testovaný projekt. V Solution Exploreru pravé tlačítko na testovaném projektu a Add / reference a zvolit příslušný projekt.

3) Vytvořme první test a pojmenujme ho TestClone.

Pozn.: Pro další předpokládejme, že máme k dispozici (již otestované) matody Same (stejná jako Equal + stejná i iCapacity), At, kopykonstruktor, Allocate, Deallocate.

Pozn.: Test končí při prvním nesplněném Assert. Proto musíme testy promyslet. V případě, že chceme oznámit všechny testované "neúspěchy", musíme napsat

více testů. V našem případě zkusme vypracovat testy dva - jeden pro inicializovanou (TestClone) a jeden pro neinicializovanou proměnnou CVector (TestCloneNoInit)

Připravte testovací metodu pro druhý test.

4) v testu přichystejte data (servisní kód) pro testovanou metodu. Vytvořte prázdnou metodu. Získejte výsledek voláním metody a vytvořte testy pomocí Assert.Napišme v metodách testy - tj. použijeme Assert::isTrue, kde hodnotou bude návratová hodnota metody Same.

Připravme (Arrange) v testovacích metodách proměnné pro oba testy. V testovacích metodách nadefinujme proměnné pro výsledek a do nich uložme výsledek metody a.Clone().V prvním vytvoříme prázdnou proměnnou (pouhá inicializace v definici).

Ve druhém případě vytvořme proměnnou např. s 2000 prvky, ze kterých bude použito 1900 (nutno dopsat metodu SetSize, úpravy třídy se dělají v původním projektu) a budou nastaveny od -500 s krokem 1.

5) Následně napište kód (metody i testů) tak aby test skončil úspěchem. Napišme metodu Clone, která vytvoří pomocnou proměnnou a tu vrátí. (Nejjednodušší metoda, kterou budeme postupně vylepšovat).

6) Snažte se kód odladit (vylepšit) tak aby byl rychlejší, přehlednější ... a test (i ostatní) zůstal "zelený"

7) Pokud je první test CloneNoInit v pořádku, můžeme pokračovat s testu u proměnné, která je inicializována.

Pozn.: Test je sice v pořádku, ale my víme, že je to náhoda. Prozatím nám to stačí, přizpůsobování kódu druhému testu způsobí, že se stane no init test "červeným" a potom se mu budeme věnovat.

14) Postupně rozšiřme Clone - pokud jsou data nullptr, vraťme neinicializovanou proměnnou. Spusťme testy - zhodnoťme.

Nastavme ve vracené proměnné iCapacity - otestujme.

Nastavme ve vracené proměnné iSize - otestujme.

Alokujme paměť - otestujme.

Překopírujme data - otestujme.

15) Proveďme kontrolu alokované paměti pomocí check. (Příklad kódu testů viz. výše)

16) Promysleme, zda by nebylo možné napsat metodu Clone lépe než podle bodu 14). (Např. využít pro vytvoření návratové kopie kopykonstruktor).






poslední úpravy 2018-11-13