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

About

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

  1. Porozumieť základom agilnej metodiky vývoja riadeného testami.
  2. Naučiť sa vytvárať testy v jazyku C pomocou knižnice check
  3. Vytvoriť vlastné jednotkové (unit) testy funkcií.
  4. Naučiť sa orgarnizovať štruktúru testov pomocou vzoru AAA.
  5. Naučiť sa spúšťať pravidlá z iného Makefile sú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ípad is_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 all

dô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.

Úloha

V priečinku tests/ vytvorte kostru porebných súborov pre naše testy.

Zatiaľ vytvoríme tieto súbory:

  • Makefile, a
  • suite_hof.c a suite_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
-lcheck

Keďž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.

Ú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.

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);
}

Ú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: 0

Best 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.

Ú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_TEST

Po 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.

Ú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/ all

Upraviť 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/ clean

Additional Tasks

  1. Implementujte zostávajúce jednotkové testy pre otestovanie funkcie is_game_won().

Ďalšie zdroje