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