12. díl - Ako testovať programy v C (knižnica assert.h) 2. časť

C++ Pokročilé konstrukce Ako testovať programy v C (knižnica assert.h) 2. časť

V predchádzajúcom článku sme si povedali o testovaní v céčku a vyskúšali sme si takzvaný Glass-box Testing.

Dnes si povieme o Testami riadenom vývoji - (test-driven development - TDD).
Je to technika tvorby softvéru, ktorá bola navrhnutá s cieľom umožniť čo najrýchlejšie zistenie a opravenie chýb.Spravidla sa testy píšu ešte pred samotnou implementáciou, ktorá musí pred odovzdaním všetky testy prejsť. Vytváraný kód sa tu testuje už od začiatku programovania, nie až po dokončení celého modulu. Tento prístup by teda mal obmedziť neskoré odstraňovanie chýb na minimum, čím sa ušetria vzácne zdroje – čas, pracovná sila aj peniaze.

Tento spôsob testovanie sa nazýva aj Black-box Testing, pretože modul (funkcia) je čierna skrinka a nevidíme do nej (niekedy ešte funkcia ani nie je napísaná).

Black-box Testing

Parsing

Napadlo ma, že by sme si mohli napísať pomocnú knižnicu na parsovanie vstupov od užívateľa.

Tak ideme na to, nie? :-)

Založme si v IDE nový projekt s názvom Parsing, pridáme doňho header súbor parse.h a hlavný source súbor (obsahuje funkciu main()) premenujeme na parse.c.
Žiadny ďalší súbor nebudeme potrebovať, pretože použijeme podmienený preklad za pomoci funkcií preprocesora.

Súbor parse.h bude vyzerať takto:

#ifndef PARSING_H_INCLUDED
#define PARSING_H_INCLUDED
/**
* Funkcia skontroluje, či vstupný reťazec obsahuje iba číslice,
* ak nie, prvý odlišný znak zapíše na prvú pozíciu reťazca
* @param vstupný reťazec (vstupno - výstupný parameter)
* @return reťazec prevedený na int ak reťazec obsahuje iba číslice, inakšie 0
*/
int parse_int(char *number);
/**
* Funkcia skontroluje, či reťazec obsahuje iba jednu desatinnú bodku,
* ak ich obsahuje viac, vyhodnotí to ako chybu,
* skontroluje, či obsahuje namiesto botky čiarku, ak áno, prepíše ju na bodku,
* skontroluje, či vstupný reťazec obsahuje iba číslice (okrem jednej botky),
* ak nie, prvý odlišný znak zapíše na prvú pozíciu reťazca
* @param vstupný reťazec (vstupno - výstupný parameter)
* @return reťazec prevedený na double ak reťazec obsahuje iba číslice,
* alebo číslice a jednu bodku, inakšie 0
*/
double parse_double(char *number);
#endif // PARSING_H_INCLUDED

Súbor parse.c bude na začiatku vyzerať takto:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <assert.h>
#include "parsing.h"
#define TESTY // pri zakomentovaní budú testy odstavené

int parse_int(char *number)
{
    return atoi(number); // prevedie reťazec na int
}

#ifdef TESTY
#define TESTY

int main()
{
    char *cislo1;

    cislo1 = (char*)malloc(10);
    strcpy(cislo1, "123456");
    assert(parse_int(cislo1) == 123456);
    printf("Testy presli.\n");
    free(cislo);
}

#endif // TESTY

Uložiť, skompilovať a spustiť.

test 1

=> testy prešli. A máme hotovo! :-)

Ak chcete správne pochopiť testovanie, bolo by dobré, keby ste všetky kroky robili spolu so mnou.

Skúsme pridať ďalší test:

cislo2 = (char*)malloc(10);
strcpy(cislo2, "123456x");
assert(parse_int(cislo2) == 0);

Výstup:

test 2

A máme Assertion failed. Prečo? Reťazec obsahuje iný znak ako číslicu a návratová hodnota má byť 0 (nula).
Skúsme teda upraviť kód, napríklad takto:

if (atoi(number))
    return atoi(number);
else
    return 0;

A-a-a zase chyba. Čo sa stalo? Zistíme to tak, že si vypíšeme reťazec prevedený na číslo, pridáme výpis do funkcie main():

char *cislo2 = "123456x";
printf("retazec\t%s\ncislo\t%d\n", cislo2, atoi(cislo2));
assert(parse_int(cislo2) == 0);

Výstup:

test 3

A hneď vidíme kde je problém. Funkcia atoi() vzala do úvahy číslice, ktoré boli pred znakom x.
Ako to riešiť? Vyskúšame zmeniť hodnotu *cislo2 na "123x456" a otestujeme.
Výstup:

test 4

Problém je tu stále. Funkcia atoi() prevedie číslice, ktoré sú pred iným znakom na číslo, preto teraz presunieme ten znak na začiatok reťazca: char *cislo2 = "x123456";
Výstup:

test 5

Testy prešli a teraz už vieme, že ak je v reťazci iný znak ako číslica, treba ho vložiť na začiatok reťazca.
Vyskúšajme upraviť funkciu takto:

for (int i = 0; i < strlen(number); i++) {
    if (!isdigit(number[i])) { // znak nie je číslica
        number[0] = number[i];
        break;
    }
}
if (atoi(number)) {
    return atoi(number);
}
else
    return 0;

Výstup:

test 6

Čo sa deje? Prečo je číslo 123 a nie 0, pričom testy prešli? Všetko je v poriadku, len som zabudol zmazať (zakomentovať) riadok s výpisom a ten neberie výstupy z funkcie.

Fajn. Zdá sa, že prvá funkcia je bezchybná. Napriek tomu by možno bolo ešte dobré to poriadne otestovať napríklad tak, že do textového súboru napíšeme 100 rôznych reťazcov a tie sa budú postupne testovať.

RGR systém

Videli sme, že funkciu píšeme tak, aby prešla prvým testom skoro za každú cenu. Potom pridáme ďalší test a tak ďalej až do úspešného konca.

Tento systém sa nazýva RGR systém.

RGR systém
  • RED - napíšte malý test, ktorý nefunguje a na prvýkrát možno ani nepôjde preložiť
  • GREEN - rýchlo spojazdnite test a neváhajte pri tom urobiť hocijakú somarinu
  • REFACTOR - zbavte sa všetkých duplicít vytvorených v snahe spojazdniť test

A tak stále dookola, kým nie je kód bezvadný.

Aby sme sa s tým lepšie stotožnili, TDD spôsobom si napíšeme aj druhú funkciu.

Pridáme si ju do súboru parsing.c:

double parse_double(char *number)
{

}

a napíšeme prvý test.

/** testy funkcie int parse_double(char *number) **/
char *cislo3;

cislo3 = (char*)malloc(10);
strcpy(cislo3, "123.456");
assert(parse_double(cislo3) == 123.456);

Nedá sa to scompilovať, pretože vo funkcii nemáme návratovú hodnotu. Doplníme.

return atof(number);

Skompilujeme a spustíme. Prešli. Máme hotovo! :-)

Samozrejme, že nie. Ideme nájsť ďalšie špeciálne zadanie od užívateľa.

strcpy(cislo3, "1.23.45.6");
assert(parse_double(cislo3) == 0);

Assertion failed! V reťazci môže byť okrem číslic maximálne jedna bodka (tečka). Čo s tým?

int point = 0;
for (int i = 0; i < strlen(number); i++) {
    if (number[i] == '.')
        point++;
}
if (point > 1)
    return 0;
else
    return atof(number);

Testy prešli. Už zase máme hotovo. :-` Ďalšia užívateľova špecialitka:

strcpy(cislo3, "123,456");
assert(parse_double(cislo3) == 123.456);

V tomto prípade chceme, aby to vzalo aj tú čiarku namiesto botky.
Assertion failed!
Odstránime chybu tým, že doplníme do funkcie.

for (int i = 0; i < strlen(number); i++) {
    if (number[i] == ',')
        number[i] = '.';
}

Testy prešli. Skúsme toto:

strcpy(cislo3, "123,45,6");
assert(parse_double(cislo3) == 0);

Assertion failed! Prečo? Vo funkcii treba najskôr ošetriť čiarky a až potom botky. A bude to v poriadku.

Ďalšie špatné zadanie môže byť, že tam budú znaky mimo číslic, čiarok a bodiek.

strcpy(cislo3, "12x3.45,6");
assert(parse_double(cislo3) == 0);

Samozrejme, že je zase chyba. Vyriešime pridaním kódu:

for (int i = 0; i < strlen(number); i++) {
    if (!isdigit(number[i])) {
        number[0] = number[i];
        break;
    }
}

Testy prešli.
Ale skúsme ešte niečo:

strcpy(cislo3, "12x3456");
assert(parse_double(cislo3) == 0);

Nebude tam žiadna bodka, čo to spraví? Testy prešli.

Fajn. Zdá sa, že aj druhá funkcia je bezchybná. Napriek tomu by možno bolo ešte dobré to poriadne otestovať napríklad tak, že do textového súboru napíšeme 100 rôznych reťazcov a tie sa budú postupne testovať.

Pozor dôležité!
Nikdy nezabudnite nakoniec upraviť kód tak, aby bol čo najjednoduchší, aby sa neopakovali rovnaké časti kódu a aby bol dobre okomentovaný. (Môj je priložený.)

Teraz môžeme vypnúť testovanie tak, že zakomentujeme riadok:

//#define TESTY // pri zakomentovaní budú testy odstavené

a nepodarí sa nám to ani skompilovať, pretože projektu chýba funkcia main().
To nateraz nevadí. Niekedy, keď budete tieto dve funkcie potrebovať, stačí do projektu pridať obidva súbory a v súbore, v ktorom sa budú funkcie používať, includovať parsing.h.

Takže ako vidíte, testovanie nás núti premýšľať aké všemožné vstupy sa môžu do funkcie dostať a aké to môže mať následky na beh programu a núti nás to upraviť kód tak, aby sme všetky problémy už dopredu vyriešili.

Samozrejme že, vývoj riadený testami určite nie je univerzálnym liekom na všetko a dosiahnutie želaného efektu v žiadnom prípade nie je zaručené. Ale je to najlepšia cesta, ako sa maximálne vyhnúť možným problémom s nefunkčnosťou projektov u konečného užívateľa.

Keď sa nad tým zamyslím, vlastne vývoj všetkého sa testuje. Počnúc hračkami pre deti, cez napríklad bielu techniku, autá, zbrane až po rakety. Kto netestuje, väčšinou predáva nekvalitné šunty a preto rýchlo skončí svoju kariéru.

Ja vám všetkým držím palce pri písaní svojich projektov pomocou TDD.

Na záver prikladám zdrojové kódy napísané v Linux Ubuntu.


 

Stáhnout

Staženo 94x (9.15 kB)
Aplikace je včetně zdrojových kódů v jazyce c

 

  Aktivity (3)

Článek pro vás napsal Libor Šimo (libcosenior)
Avatar
Obľúbil som si jazyk C a snažím sa ho spoznať čo najhlbšie. Začal som používať c# WPF, je to krása.

Jak se ti líbí článek?
Ještě nikdo nehodnotil, buď první!


 



 

 

Komentáře

Avatar
Kozino
Člen
Avatar
Kozino:

Len taká malá poznámka k správnym programátorským návykom - vždy keď sa použije malloc treba použiť aj free (aj keď je to len malý ukážkový kód), nie je dobré si zbytočne robiť memory leak-y a spoliehať sa na OS že on to vyčistí po konci programu. ;-)

 
Odpovědět  +1 30.6.2014 10:14
Avatar
Neaktivní uživatel:

Použiť free/delete po malloc/new je absolútna povinnosť pri jazykoch C/C++, ktoré nemajú automatickú správu pamäte.

Odpovědět 30.6.2014 10:35
Neaktivní uživatelský účet
Avatar
Libor Šimo (libcosenior):

Chlapci máte pravdu, zanedbal som základnú vec. Akonáhle bude čas, napravím to.
Díky za upozornenie. :)

Odpovědět 1.7.2014 14:06
Aj tisícmíľová cesta musí začať jednoduchým krokom.
Avatar
Kozino
Člen
Avatar
Kozino:

Ak chceš robiť úpravy možno by bolo dobré sa zamyslieť či by nebolo lepšie vymeniť

char *cislo1;
cislo1 = (char*)malloc(10);
strcpy(cislo1, "123456");

za

char cislo1[] = "123456";

.

 
Odpovědět 1.7.2014 18:31
Avatar
Odpovídá na Kozino
Libor Šimo (libcosenior):

To by som musel zmeniť aj funkcie a nevidím dôvod prečo by som mal so string-mi pracovať v zásobníku a nie v halde.

Odpovědět 14.2.2015 10:04
Aj tisícmíľová cesta musí začať jednoduchým krokom.
Avatar
Odpovídá na Kozino
Libor Šimo (libcosenior):

Opravil som to v článku aj v zdrojovom kóde. Je to teraz síce napísané v linuxe, ale to hádam nevadí. ;-)
Čaká to na schválenie redakciou.

Editováno 14.2.2015 10:07
Odpovědět 14.2.2015 10:06
Aj tisícmíľová cesta musí začať jednoduchým krokom.
Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zobrazeno 6 zpráv z 6.