Test-Driven Development
Vývoj riadený testami, black-box testing, knižnica na tesovanie
check, dizajn testov, menné konvencie, odporúčania pre
testovanie, volanie pravidiel iného Makefile súboru.
Videá pre cvičenie
Upozornenie
Pred príchodom na cvičenie si nezabudnite stiahnuť kostru zadania K, nakoľko testy budeme vytvárať pre toto zadanie. Ak kostru nebudete mať, nie ste dostatočne pripravení na cvičenie!
Upozornenie
Predtým, ako prídete na cvičenie, si overte, či máte nainštalovanú
knižnicu check. Ak ju
nainštalovanú nemáte, postupujte podľa nasledujúcich pokynov:
# Ak používate linuxovú distribúciu Fedora, balík nainštalujete nasledovne:
$ sudo dnf install check-devel
# Ak používate linuxovú distribúciu Ubuntu, balík nainštalujete nasledovne:
$ sudo apt install check
# Ak používate Mac OS, balík nainštalujete nasledovne:
$ brew install checkAbout
Jedna z agilných metodík, ktorá je v praxi bežne používaná, sa nazýva vývoj riadený testami (z ang. test driven development), kedy sa voči známemu rozhraniu najprv napíšu testy a až tak sa vytvára samotná implementácia. Výsledný kód a jeho aktualizácie sú tak neustále kontrolované voči očakávanému správaniu zapísanému v testoch. Ak dôjde zmenou kódu ku chybe, tá je okamžite odhalená pri spustení testov.
Túto metodiku používame aj pri testovaní vašich zadaní v Aréne a na tomto cvičení vám pomôžeme si ju osvojiť.
Objectives
- Porozumieť základom agilnej metodiky vývoja riadeného testami.
- Naučiť sa vytvárať testy v jazyku C pomocou knižnice
check - Vytvoriť vlastné jednotkové (unit) testy funkcií.
- Naučiť sa orgarnizovať štruktúru testov pomocou vzoru AAA.
- Naučiť sa spúšťať pravidlá z iného
Makefilesúboru.
Postup
Test Design
Ešte predtým, ako sa pustíme do implementácie testov, sa zoznámime s
terminológiou a navrhneme si niekoľko testov funkcie
is_game_won() z modulu K. Neskôr budeme tieto
testy aj implementovať.
Úloha
Slovne navrhnite testy pre overenie správnej činnosti funkcie
is_game_won().
Testy budeme navrhovať ako tvrdenia (z angl. assertion). To znamená, že vyslovíme tvrdenie, ktoré sa budeme následne snažiť overiť implementovaním jednotkových testov.
Príklad takéhoto tvrdenia môže vyzerať nasledovne:
Ak je herná plocha prázdna, tak vráti
false.
Úloha
Zoznámte sa so základnou terminológiou pre testovanie a to konkrétne - čo je to jednotkový test (z angl. unit test), testovací prípad (z angl. test case) a sada testov (z angl. test suite).
Význam týchto termínov je (aj) v kontexte nášho projektu nasledovný:
- Jednotkový test - označuje konkrétny test, ktorým
testujeme jednu konkrétnu funkcionalitu.
- je reprezentovaný jednou funkciou
- testuje sa v ňom len jedno tvrdenie
- každý test musí byť atomický a izolovaný od ostatných - musí sa dať kedykoľvek spustiť nezávisle od ostatných testov
- Testovací prípad - označuje sadu jednotkových
testov, ktoré sa spúšťajú spolu na otestovanie konkrétneho prípadu.
- miesto jedného testu, v ktorom sa snažíte overiť viacero tvrdení, vytvoríte viacero testov a zoskupíte ich do samostatného testovacieho prípadu
- Sada testov - zbierka viacerých testovacích
prípadov, ktoré môžu byť zoskupené napr. na základe modulu, funkcie,
logiky a pod.
- overujú väčšiu časť
V našom prípade:
- vytvoríme jednotkové testy, kde každý implementujeme ako samostatnú funkciu
- vytvoríme jeden testovací prípad, ktorým bude funkcia
is_game_won()a všetky jednotkové testy budú súčasťou tohto prípadu - vytvoríme jednu sadu testov pre modul
K, do ktorého vložíme testovací prípadis_game_won()
Warm up!
Predtým, ako sa vrhneme do tvorby samotných testov, pripravíme si pracovné prostredie. Ak ste tak ešte neurobili, tak si v tomto kroku stiahnete svoj projekt z git-u, rozbalíte v ňom kostru projektu zadania K a počas cvičenia budete pracovať v jeho priečinku.
Úloha
Ak ste tak ešte neurobili, tak do vášho repozitára pridajte kostru projektu K.
Kostru projektu umiestnite do priečinku ps3/ vášho
repozitára. Zmeny nezabudnite odoslať do Git-u.
Úloha
V priečinku ps3/, ktorý obsahuje zdrojové kódy zadania
K vytvorte priečinok
tests/, do ktorého budeme ukladať naše testy.
Poznámka
Do tohto priečinku budeme ukladať vlastné testy pre zadanie K. Pre úspešné odovzdanie a hodnotenie projektu však tento priečinok nie je dôležitý. Ak ho teda do Git repozitára nepridáte, nebudete za to nijakým spôsobom postihnutí v hodnotení.
Úloha
V priečinku tests/ vytvorte kostru porebných súborov pre
naše testy.
Zatiaľ vytvoríme tieto súbory:
Makefile, asuite_hof.casuite_k.c- v týchto súboroch sa budú nachádzať sady testov pre jednotlivé moduly.
V priečinku tests/ ich vytvorte napríklad pomocou
príkazu touch.
Úloha
Prekopírujte a upravte súbor Makefile z koreňového
priečinku projektu do priečinku s testami tests/.
CC = /usr/bin/gcc
CFLAGS = -Wall -Werror -std=c11 -g
LDLIBS = -lm
TARGET = tests
OBJS = suite_k.o suite_hof.o
.PHONY = all build clean
all: build
build: $(OBJS)
$(CC) $(CFLAGS) $^ $(LDLIBS) -o $(TARGET)
%.o: %.c
@printf "Building $<\n"
$(CC) $(CFLAGS) -c $< -o $@
clean:
@printf "Cleaning Tests\n"
rm -rf $(TARGET) *.oÚloha
Aktualizujte premennú LDLIBS o hodnotu, ktorú vráti
nástroj pkg-config.
Pri zostavovaní (linkovaní) výslednej aplikácie je potrebné pripojiť
aj knižnicu check. Spôsob pripojenia však môže byť na
každom systéme iný. Aby bolo možné zistiť, ako pripojiť pomocou linkera
potrebné knižnice na vašom systéme, použijeme nástroj
pkg-config (prípadne skrátene pkgconf).
pkgconf je program, pomocou ktorého je možné nastaviť
príznaky prekladača a linkera pre použitie knižníc vo vývoji.
Napríklad - v linuxovej distribúcii Fedora 41 bude výsledok vyzerať nasledovne:
$ pkg-config --cflags --libs check
-lcheckNásledná aktualizácia premennej LDLIBS v súbore
Makefile bude vyzerať takto:
LDLIBS = -lm -lcheckTest Suite for Module K
V tomto kroku vytvoríme prvý jednotkový test pre overenie funkčnosti
funkcie is_game_won(). Tento test následne pridáme do
testovacieho prípadu funkcie is_game_won() a ten nakoniec
zaradíme do testovacej sady modulu K.
Upozornenie
V tomto kroku ešte test nebudeme vedieť spustiť.
Úloha
Vytvorte jednotkový test pre tvrdenie: “Ak je herná plocha
prázdna, tak vráti false”.
Jednotkový test zatiaľ nazveme test1 a implementujeme ho
pomocou vzoru AAA. Na overenie tvrdenia použijeme makro ck_assert_msg().
Implementácia tohto testu bude vyzerať nasledovne:
#include <check.h>
#include "../k.h"
START_TEST(test1){
// Arrange
bool expected = false;
struct game game = {
.board = {
{' ', ' ', ' ', ' '},
{' ', ' ', ' ', ' '},
{' ', ' ', ' ', ' '},
{' ', ' ', ' ', ' '}
},
.score = 0
};
// Act
bool actual = is_game_won(game);
// Assert
ck_assert_msg(
expected == actual,
"Expected %d, but got %d.",
expected, actual
);
}END_TESTÚloha
V súbore suite_k.c vytvorte funkciu
suite_k(), ktorá vytvorí testovaciu sadu.
Novú sadu testov vytvoríme pomocou funkcie suite_create().
Táto funkcia:
- má jeden parameter, ktorým je názov sady testov, a
- vracia referenciu na novovytvorenú sadu testov, ktorá je typu
Suite.
Funkcia pre vytvorenie sady testov pre modul K bude
vyzerať takto:
Suite *suite_k(){
Suite *suite = suite_create("K");
return suite;
}Úloha
Vo funkcii suite_k() vytvorte testovací prípad s menom
funkcie is_game_won(), vložte do neho jednotkový test
test1 a testovací prípad zasa vložte do sady testov
"K".
Testovací prípad vytvoríme pomocou funkcie tcase_create().
Táto funkcia:
- má jeden parameter, ktorým je názov testovacieho prípadu, a
- vracia referenciu na novovytvorený testovací prípad typu
TCase.
Jednotkový test vložíme do testovacieho prípadu pomocou makra tcase_add_test().
Samotný testovací prípad vložíme do testovacej sady pomocou funkcie suite_add_tcase().
Aktualizovaná funkcia bude vyzerať nasledovne:
Suite *suite_k(){
TCase *tcase = tcase_create("is_game_won()");
tcase_add_test(tcase, test1);
Suite *suite = suite_create("K");
suite_add_tcase(suite, tcase);
return suite;
}Running Tests with Test Runner
V predchádzajúcich krokoch sme vytvorili jednotkový test
test1. Následne sme vytvorili testovací prípad s názvom
is_game_won(), do ktorého sme vytvorený jednotkový test
vložili. Nakoniec sme vytvorili sadu testov pre modul K a
vytvorený testovací prípad spolu s jednotkovým testom sme vložili do
neho.
Zatiaľ však test spustiť nevieme. To sa však zmení v tomto kroku, pretože vytvoríme chýbajúci prvok, ktorým je spúšťač testov, tzv. test runner.
Úloha
Vytvorte súbor runner.c a vo funkcii main()
vytvorte spúšťač testov.
Spúšťač testov vytvoríme pomocou funkcie srunner_create().
Táto funkcia:
- má jeden parameter, ktorým je referencia na sadu testov, a
- jej návratovou hodnotou je referencia na objekt typu
SRunner.
Poznámka
Ak budete potrebovať pridať do spúšťača testov novú sadu testov,
môžete to urobiť pomocou funkcie srunner_add_suite().
Napríklad:
srunner_add_suite(runner, suite_hof());Testy spustíme zavolaním funkcie srunner_run_all() a po
ich vykonaní uvoľníme spotrebovanú pamäť zavolaním funkcie
srunner_free().
Výsledná implementácia bude vyzerať nasledovne:
#include <check.h>
int main(){
SRunner *runner = srunner_create(suite_k());
srunner_run_all(runner, CK_NORMAL);
srunner_free(runner);
}Poznámka
Súbor so spúšťačom by sme mohli nazvať aj main.c, ale
aby sme ho vedeli odlíšiť explicitne, nazveme ho
runner.c.
Úloha
Preložte a spustite testy pomocou vytvorených pravidiel v súbore
Makefile.
Ak ste postupovali správne a preklad prebehne v poriadku, tak v
priečinku tests/ nájdete spustiteľný súbor
tests. Týmto súborom spustíte spúšťač testov, ktorý spustí
všetky testy. V opačnom prípade opravte vzniknuté chyby.
Obrazovka bude po spustení vyzerať takto:
$ ./tests
Running suite(s): K
100%: Checks: 1, Failures: 0, Errors: 0Best Practices
Na záver
Úloha
Upravte názov testu
if-then
when-expect
Úloha
Vytvorte test s názvom segfault, ktorý overí správanie
knižnice v prípade vzniku chyby segmentácie.
Test pre overenie chyby segmentácie môže vyzerať napríklad takto:
START_TEST(segfault){
struct game *game = NULL;
game->score = 9999;
}END_TESTPo pridaní testu do testovacieho prípadu a spustení testov uvidíte, že tento prípad bude zachytený ako test, ktorý zlyhal (resp. test, v ktorom došlo ku chybe):
$ ./tests
Running suite(s): K
33%: Checks: 3, Failures: 1, Errors: 1
suite_k.c:54:F:is_game_won():if_board_contains_k_expect_true:0: Expected 1, but received 0.
suite_k.c:6:E:is_game_won():segfault:0: (after this point) Received signal 11 (Segmentation fault)Knižnica check totiž
spúšťa testy v samostatných procesoch (fork). Pokiaľ dôjde ku chybe
segmentácie, ukončí sa iba proces, v ktorom ku chybe došlo. Spúšťač
testov takýto test označí ako test, ktorý zlyhal.
Poznámka
V prípade, že potrebujete toto správanie vypnúť, môžete tak urobiť nasledujúcim volaním:
srunner_set_fork_status(runner, CK_NOFORK);Úloha
Makefile z ineho makefile
Ak sa nástroj make spustí s voľbou
--directory, pred načítaním súboru Makefile
prejde do cieľového priečinku. Vďaka tomu môžeme súbore
Makefile v koreňovom priečinku projektu vytvoriť pravidlo
tests, ktoré zostaví testy v priečinku
tests/.
Na volanie nástroja make pritom môžeme použiť vnútornú
premennú $(MAKE), ktorá obsahuje názov aktuálne spusteného
nástroja make.
Pravidlo tests na spustenie testov z priečinku
tests/ bude vyzerať takto:
tests: $(OBJS)
$(MAKE) --directory tests/ allUpraviť môžeme aj pravidlo clean pre vyčistenie projektu
- okrem vyčistenia koreňového priečinku projektu ho môžeme rozšíriť o
vyčistenie priečinku s testami. Upravené pravidlo teda môže vyzerať
nasledovne:
clean:
@printf "Cleaning\n"
rm -rf $(TARGET) *.o
$(MAKE) --directory tests/ cleanAdditional Tasks
- Implementujte zostávajúce jednotkové testy pre otestovanie funkcie
is_game_won().
Ďalšie zdroje
Knižnica
check- domovská stránka projektuThe Arrange, Act, and Assert (AAA) Pattern in Unit Test Automation - opis vzoru AAA pre lepšie organizovanie testov