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

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

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
-lcheck

Následná aktualizácia premennej LDLIBS v súbore Makefile bude vyzerať takto:

LDLIBS = -lm -lcheck

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

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.

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 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_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, 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/ 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