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. Vytvorte aj všetky chýbajúce súbory včetne súboru
Makefile, ktorý môžete použiť z vašich predchádzajúcich
projektov alebo môžete použiť túto jednoduchú verziu:
CC = /usr/bin/gcc
CFLAGS = -Wall -Werror -std=c11 -g
LDLIBS = -lm
TARGET = k
OBJS = k.o hof.o main.o
.PHONY = all build clean tests
all: build
build: $(OBJS)
$(CC) $(CFLAGS) $^ $(LDLIBS) -o $(TARGET)
%.o: %.c
@printf "Building $<\n"
$(CC) $(CFLAGS) -c $< -o $@
clean:
@printf "Cleaning\n"
rm -rf $(TARGET) *.oÚloha
Uistite sa, že sa prgram dá preložiť.
Ak ste postupovali správne, napísaním príkazu
$ make clean alldôjde k vytvoreniu spustiteľného súboru k.
Ú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/.
V prípade, že prekopírujete len súbor Makefile z
priečinku so zdrojovými súbormi, stačí upraviť tieto premenné:
TARGET = tests
OBJS = suite_k.o suite_hof.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 42 bude výsledok vyzerať nasledovne:
$ pkg-config --cflags --libs check
-lcheckKeďže sa však napríklad v distribúcii Ubuntu môže tento
zoznam líšiť, samotné uvedenie voľby -lcheck by spôsobilo
neprenositeľnosť súboru Makefile. Aby sme sa tomu vyhli,
zahrnieme výstup príkazu pkg-config do definície premenných
CFLAGS a LDLIBS:
LDLIBS = -lm $(shell pkg-config --libs check)
CFLAGS = -Wall -Werror -std=c11 -g $(shell pkg-config --cflags check)Test 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
V súbore suite_k.c 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
tcase_is_game_won(), ktorá vytvorí testovací
prípad pre otestovanie funkcie is_game_won().
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().
Funkcia bude vyzerať nasledovne:
TCase* tc_is_game_won(){
// create test case
TCase* tcase = tcase_create("is_game_won()");
// populate test case with unit tests
tcase_add_test(tcase, test1);
return tcase;
}Ú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(){
// create test suite
Suite *suite = suite_create("K");
return suite;
}Úloha
Do sady testov s názvom "K" pridajte
testovací prípad s menom `“is_game_won()”
Testovací prípad vložíme do testovacej sady pomocou funkcie suite_add_tcase().
Aktualizovaná funkcia bude vyzerať nasledovne:
Suite *suite_k(){
// create test suite
Suite *suite = suite_create("K");
// populate test suite with test cases
suite_add_tcase(tcase, tc_is_game_won());
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 vytvoreného testu.
Ak test zlyhá, jeho názov je súčasťou chybového výstupu. Preto je dôležité, aby testy mali zmysluplné názvy a bolo na prvý pohľad jasné, čo presne testujú.
Existuje mnoho konvencií, ktoré je možné použiť. Ako začiatočníkom vám odporúčame používať jednoduchú konvenciu, kedy názov testu opíšete ako vetu v tvare:
if-then
alebo rozličnými variáciami ako
when-expect
V našom prípade, kedy test znie po slovenky takto
“Ak je herná plocha prázdna, tak vráti false.”
ho môžeme previesť na identifikátor funkcie v jazyku C napríklad takto:
test_if_game_board_is_empty_then_return_false
Miesto nič nehovoriaceho názvu testu test1 máme názov
funkcie, ktorý priamo opisuje správanie testu. A v prípade, že test
zlyhá, uvidíme opis očakávaného správania už vo výpise chyby.
Poznámka
Všimnite si, že identifikátor testu má prefix test_.
Hlavne v knižniciach vyšších jazykov je tento zápis povinný, ak chcete,
aby bola funkcia považovaná za test. Test Runner totiž
identifikuje sám, ktoré funkcie testy reprezentujú, a ktoré nie. Táto
identifikácia sa deje práve na základe uvedeného prefixu (alebo
postfixu, v závislosti od knižnice a jej nastavení).
V našom prípade funkcie, ktoré reprezentujú testy, vložíme do testovacieho prípadu ručne, takže na názve až tak nezáleží. Je to však konvencia, ktorú je dobré si osvojiť.
Ú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, ešte
pred načítaním súboru Makefile sa prepne do cieľového
priečinka. Vďaka tomu môžeme v súbore Makefile v koreňovom
priečinku projektu definovať pravidlo tests, ktoré zostaví
testy v priečinku tests/.
Na spustenie nástroja make môžeme použiť vnútornú
premennú $(MAKE), ktorá obsahuje názov aktuálne spusteného
programu (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