10. týždeň

Tvorba jednotkových testov

Základy testovania pomocou jednotkových testov, ich význam a použitie techniky Test Driven Development.

Ciele

  1. Pochopiť význam automatizovaného testovania a oboznámiť sa so základnými pojmami v oblasti unit testov.
  2. Naučiť sa písať jednoduché unit testy.

Úvod

Úvod do vytvárania jednotkových testov

Postup

Krok 1: Význam automatizovaného testovania

  • Hlavným cieľom testov je predísť vzniku chýb v programe. Pomocou testov overujeme, či sa program správa podľa našich očakávaní. Vďaka tomu, že sa testy môžu vykonávať automaticky, vieme nájsť chyby, ktoré by sme letmým používaním programu nenašli.
  • Testy predstavujú špecifikáciu, ktorá je zapísaná v programovacom jazyku, a je teda vykonateľná. To znamená, že je možné automaticky vyhodnotiť, či program zodpovedá alebo nezodpovedá špecifikácii. (Samozrejme, nie je možné zachytiť v nej úplne všetky aspekty softvéru.)
  • Zmeny v zdrojovom kóde môžeme robiť s väčšou istotou, že program aj naďalej funguje správne. Vďaka tomu môžeme byť oveľa flexibilnejší pri zlepšovaní štruktúry kódu a jeho ďalšom vývoji.
  • Testy tiež môžeme chápať ako istú formu dokumentácie. Test často obsahuje jednoduché príklady konkrétnych vstupov a zodpovedajúcich výstupov. Tie môžu byť jednoduchšie na pochopenie ako zdĺhavý textový popis alebo kód testovanej funkcie.

Automatizované testy sú dnes nevyhnutnou súčasťou vývoja softvéru. Aby sa zabezpečila vysoká miera otestovania programu, je potrebné testy vyvíjať súčasne s kódom programu, k čomu vedie technika [Test Driven Development][tdd-paper].

Krok 2: Základné pojmy

Automatizovane môžeme testovať systém ako celok, jeho väčšie súčasti, alebo každú jednotku osobitne. Na tomto cvičení sa oboznámime práve s testami jednotiek („unit tests“). Každý test testuje práve jednu „jednotku“ (unit). Čo táto jednotka znamená, záleží od konkrétneho prípadu: môže sa jednať o funkciu/metódu, sadu príbuzných funkcií/metód, modul, triedu, či sadu príbuzných tried.

Pre zjednodušenie vytvárania jednotkových testov sa väčšinou používajú špecializované knižnice alebo rámce (framework). My budeme používať veľmi jednoduchú knižnicu greatest, ktorá poskytuje základné prostriedky pre definovanie testov, ich organizáciu a spúšťanie.

Keďže jednotiek je v systéme veľa, aj testov býva väčšie množstvo a je potrebné nejako ich usporiadať. V našom prípade bude jednotkou (unit) funkcia. Knižnica greatest umožňuje definovať viacero súprav testov (test suite). Každá súprava býva uložená v samostatnom súbore a obsahuje niekoľko testov, pričom jeden test je reprezentovaný jednou funkciou.

Poznámka

Iné knižnice môžu používať mierne odlišnú organizáciu. Napr. JUnit pre jazyk Java označuje jeden súbor (triedu) ako test case. Tie sa potom môžu voliteľne zhlukovať do súprav (test suite).

Úlohou testu je overiť, či testovaná jednotka pri daných vstupoch dáva správny výstup. Na to používa tzv. tvrdenia (assertions). Ak niektoré z tvrdení nie je pravdivé, daný test je neúspešný (FAIL). V opačnom prípade je úspešný (PASS).

Krok 3: Implementácia jednotkových testov

Úloha 3.1

Stiahnite si program Notwork a oboznámte sa s ním.

Na predvedenie tvorby unit testov budeme používať zdrojový kód programu Notwork. Stiahnite si zdrojový kód pomocou git-u (nepotrebujete nastavovať kľúče, ide o anonymný prístup na čítanie):

$ git clone https://git.kpi.fei.tuke.sk/zsi-labs/notwork.git

Otvorte stiahnutý projekt, oboznámte sa so zdrojovými súbormi a skúste program spustiť a používať.

Notwork je jednoduchá sociálna sieť s konzolovým rozhraním. Má tú výhodu, že funguje aj offline. Nevýhodou je, že podporuje len jedného používateľa, príspevky si teda budete musieť "lajkovať" sami. Na rozdiel od bežných sociálnych sietí, jeden príspevok môže mať aj viacero "lajkov" od jedného používateľa.

Poznámka

Na preloženie programu potrebujete mať nainštalovaný GNU Make a GCC. Inštalácia týchto nástrojov bola opísaná v prvom cvičení.

Úloha 3.2

Oboznámte sa s existujúcimi testami.

Testy sú implementované ako funkcie, ktorých definícia sa začína makrom TEST. Vo vnútri testu sa overí jedno alebo viac tvrdení (assertion) pomocou makra ASSERT, alebo niektorého z ďalších pomocných makier a test sa ukončí makrom PASS(). Napríklad takýto test sa nachádza v súbore test_wall.c:

TEST empty_wall_should_have_no_posts() {
    wall_t *wall = create_wall();
 
    ASSERT_EQ(NULL, get_post(wall, 0));
    ASSERT_EQ(NULL, get_post(wall, 1));
 
    destroy_wall(wall);
    PASS();
}

Môžete si všimnúť, že väčšina testov má rovnakú štruktúru — sú zložené zo 4 častí:

  1. príprava údajov (v príklade je to vytvorenie nástenky),
  2. vykonanie testovanej operácie nad údajmi (volanie get_post),
  3. kontrola, či operácia má očakávaný výsledok (ASSERT_EQ),
  4. upratanie (uvoľnenie pamäte).

Niekedy sú prvé tri fázy označované ako Given-When-Then.

Úloha 3.3

Spustite testy projektu Notwork.

Na spustenie testov môžete použiť pripravený cieľ test v priloženom súbore Makefile:

$ make test

Krok 4: Tvorba jednoduchého unit testu

Úloha 4.1

V súprave (test suite) s názvom test_post vytvorte test pre funkciu like_count_text zo súboru post.c.

Funkcia like_count_text, ktorú budeme testovať, vráti text reprezentujúci počet "like-ov" daného príspevku, pričom rozlišuje medzi jednotným ("1 like") a množným číslom ("0 likes", "2 likes", "3 likes", ...). Testy môžu byť nasledovné:

TEST like_count_text_should_use_plural_for_0() {
    char buf[LIKE_COUNT_BUFFER_SIZE];
    post_t *post = create_post("new post");
 
    ASSERT_STR_EQ("0 likes", like_count_text(post, buf));
 
    destroy_post(post);
    PASS();
}

TEST like_count_text_should_use_singular_for_1() {
    char buf[LIKE_COUNT_BUFFER_SIZE];
    post_t *post = create_post("new post");
    like_post(post);

    ASSERT_STR_EQ("1 like", like_count_text(post, buf));
 
    destroy_post(post);
    PASS();
}

V prvom unit teste sme vytvorili nový príspevok a porovnali sme skutočný výstup funkcie like_count_text s očakávaným výstupom. V druhom sme navyše zavolaním funkcie like_post zvýšili počet "lajkov" príspevku na 1 a tiež sme overili zhodu výstupu testovanej funkcie s očakávaným reťazcom.

Poznámka

Nezabudnite pridať nové testy do zoznamu testov súpravy:

SUITE(test_post) {
    RUN_TEST(like_post_increments_like_count);
    RUN_TEST(like_count_text_should_use_plural_for_0);
    RUN_TEST(like_count_text_should_use_singular_for_1);
}

Úloha 4.2

Doplňte test pre overenie prípadu, že príspevok má 2 "lajky".

Krok 5: Význam unit testov pri refaktorizácii

Refaktorizácia (refactoring) je zmena zdrojového kódu bez zmeny správania výsledného programu. Jej cieľom je dosiahnuť prehľadnejší, kratší a ľahšie udržiavateľný kód, zníženie využitia zdrojov (čas, pamäť) a pod. Refaktorizácia je neoddeliteľnou súčasťou vývoja softvéru. Ako však zabezpečiť, že zmeny, ktoré programátor vykoná, naozaj nebudú mať vplyv na správanie programu a nezanesú do neho chyby?

Úloha 5.1

Refaktorizujte funkciu like_count_text v súbore post.c.

Hoci funkcia navonok funguje správne, trpí viacerými nedostatkami: Používa nevhodné názvy premenných, je zbytočne zložitá a neefektívna, a obsahuje nepoužitú premennú. Povedzme, že funkciu like_count_text refaktorizujeme nasledovne:

char *like_count_text(post_t *post, char *buf) {
    if (post->like_count == 1) {
        strcpy(buf, " 1 like");
    } else {
        sprintf(buf, "%d likes", post->like_count);
    }
    return buf;
}

Upravte funkciu vo vašom projekte tak, že doslovne skopírujete horeuvedenú implementáciu. Na prvý pohľad môže vyzerať v poriadku. Skúste však spustiť testy. Výsledkom bude zlyhanie. Zlyhaný unit test nás takpovediac upozornil, že refaktorizácia neprebehla správne.

Úloha 5.2

Opravte refaktorizovanú funkciu like_count_text tak, aby jej správanie zodpovedalo unit testu.

Zdroje

  1. Dokumentácia knižnice greatest

Doplňujúce zdroje

  1. Unit testy - blog M. Fowlera