2. Testy jednostkowe

Biblioteka Moss posiada w swoim zbiorze modułów plik, zawierający funkcje, ułatwiające tworzenie testów jednostkowych. Testowanie kodu jest stosunkowo ważną rzeczą w trakcie tworzenia oprogramowania. Jak to niektórzy mówią - lepiej zapobiegać niż leczyć. Testy jednostkowe wydłużają czas tworzenia oprogramowania, jednak mogą zaoowocować w przyszłości, wykrywając błędy w zmianach wprowadzonych w kodzie modułu, do którego test został napisany. Niejednokrotnie zdarza się nawet że niektóre błędy wykrywane są już podczas jego tworzenia.

Moduł MSTest odpowiedzialny za tworzenie testów jednostkowych jest stosunkowo prosty w użytkowaniu. Wiele przykładów jego użycia można znaleźć w folderze z testami jednostkowymi dla biblioteki. Do rozpoczęcia, oczywiście będzie potrzebny kod, który ma być testowany. Poniżej podany zostanie pełny przykład wykorzytania modułu do tworzenia testów jednostkowym.

Testowany plik:

int f_add( int a, int b )
{
    return a + b;
}
int f_sub( int a, int b )
{
    return b - a;
}
int f_mul( int a, int b )
{
    return a * b;
}

Aby przetestować powyższy kod, należy najpierw utworzyć funkcje testujące dla niego. Nie ma żadnych reguł dotyczących tego, jak dobry test ma wyglądać, jednak tworzenie testu dla każdej funkcji jest dobrym rozwiązaniem. Dobry test, to test, który próbuje wywołać funkcję na wszystkie możliwe sposoby. Oczywiście nie zawsze jest to możliwe.

Przykład funkcji testujących:

/* test dla dodawania */
int f_test_add( MST_FUNCTION *info )
{
    struct EXTRADATA *data = (struct EXTRADATA*)info->Data;
    mst_prepare( info );

    data->result = f_add( data->a, data->b );
    mst_assert_sint( data->result, ==, 5 );

    return MSET_OK;
}
/* test dla odejmowania */
int f_test_sub( MST_FUNCTION *info )
{
    struct EXTRADATA *data = (struct EXTRADATA*)info->Data;
    mst_prepare( info );

    data->result = f_sub( data->a, data->b );
    mst_assert_sint( data->result, ==, 1 );

    return MSET_OK;
}
/* test dla mnożenia */
int f_test_mul( MST_FUNCTION *info )
{
    struct EXTRADATA *data = (struct EXTRADATA*)info->Data;
    mst_prepare( info );

    data->result = f_mul( data->a, data->b );
    mst_assert_sint( data->result, ==, 6 );

    return MSET_OK;
}

Utworzenie funkcji testujących napisany kod, jest już większą połową sukcesu. Teraz wystarczy zainicjalizować struktury, odpowiedzialne za testy. W tym momencie można wybrać jedno z dwóch rozwiązań - albo wywoływać każdy test pojedynczo, albo wszystkie na raz po przypisaniu ich do struktury zestawu. Poniżej zaprezentowana została druga opcja. Warto się zastanowić, czy funkcje dostępne w strukturze zbioru, przekazywane do pól MST_SUITE.Setup oraz MST_SUITE.TearDown nie ułatwią wykonywania testów i zarządzania pamięcią.

Pozostała część kodu:

struct EXTRADATA {
    int a, b, result;
};

int f_test_add( MST_FUNCTION *info );
int f_test_sub( MST_FUNCTION *info );
int f_test_mul( MST_FUNCTION *info );

/* funkcja przygotowująca dane */
int setup_op_function( MST_SUITE *info )
{
    struct EXTRADATA *data;

    if( info->Data )
        return MSEC_INVALID_ARGUMENT;

    info->Data = malloc( sizeof(struct EXTRADATA) );
    data = (struct EXTRADATA*)info->Data;

    data->result = 0;
    data->a      = 3;
    data->b      = 2;

    return MSEC_OK;
}

/* funkcja zwalniająca pamięć po danych */
void teardown_op_function( MST_SUITE *info )
{
    free( info->Data );
}

/* funkcja główna, uruchamiająca test */
int main( int argc, char **argv )
{
    MST_FUNCTION operation_funcs[] = {
        { MST_STRINGIFY(f_test_add), "Add two numbers", NULL },
        { MST_STRINGIFY(f_test_sub), "Substract two numbers", NULL },
        { MST_STRINGIFY(f_test_mul), "Multiply two numbers", NULL },
        { MST_LASTRECORD }
    };

    MST_SUITE operation_suite = {
        "Test add, sub and mul functions",
        FALSE,                    /* BreakOnError */
        setup_op_function,        /* Setup */
        teardown_op_function,     /* TearDown */
        NULL,
        operation_funcs
    };

    /* uruchom testy w zbiorze */
    return mst_run_suite( &operation_suite );
}

Przykładowe wyjście:

===============================================================================
Test add, sub and mul functions
--------------------------------------------------------------------- [001/003]
[TEST] f_test_add
[DESC] Add two numbers
[STAT] SUCCESS! > Passed asserts: 1
--------------------------------------------------------------------- [002/003]
[TEST] f_test_sub
[DESC] Substract two numbers
[STAT] FAILED! > Passed asserts: 0
------
# Error in test.c on line 90
# data->result == 1
# Where: L = -1 and R = 1
--------------------------------------------------------------------- [003/003]
[TEST] f_test_mul
[DESC] Multiply two numbers
[STAT] SUCCESS! > Passed asserts: 1
===============================================================================

Jak można zauważyć, powyższy kod wygenerował błąd w trakcie działania funkcji f_test_sub. Przyglądając się bliżej funkcji f_sub, można zauważyć, że podczas odejmowania argumenty podane zostały na odwrót. Testy umieszczone w zestawie nie zostały przerwane podczas wystąpienia błędu, ponieważ pole MST_SUITE.BreakOnError zostało ustawione na wartość FALSE. Testy nie są zależne od siebie, więc ustawianie tego pola na wartość TRUE jest niepotrzebne. Oczywiście nie ulega wątpliwości to, że aby błąd został wykryty, test jednostkowy musi być poprawnie napisany.

2.1. Struktury

MST_FUNCTION

Struktura odpowiedzialna za przechowywanie informacji o funkcji testującej daną część kodu. Większość pól struktury w głównej mierze wykorzystywana jest w funkcji mst_run_test(), gdzie używane są podczas wyświetlania w konsoli informacji o uruchamianym teście. Można je również wykorzystać bezpośrednio wewnątrz uruchomionego testu, gdzie cała struktura przekazywana jest do argumentu funkcji testującej. Pole MST_FUNCTION.PassedAsserts zwiększane jest automatycznie za każdy razem, gdy wywoływane jest makro mst_assert.

Strukturę można inicjalizować w następujący sposób:

int additional_data = 5;

MST_FUNCTION test = {
    module_part_test_func,             /* Function */
    "testing_function",                /* Name */
    "Some description of this test.",  /* Desc */
    &additional_data,                  /* Data */
    0                                  /* PassedAsserts */
};

Inicjalizację ostatniego pola można jednak pominąć, ponieważ ustawiane jest ono automatycznie podczas uruchamiania testu. W przypadku gdy struktura nie będzie zawierać żadnych danych, warto dopisać do niej wartość NULL, zamiast zostawiać inicjalizacje na pastwę losu kompilatora. Jest to ważna rada w przypadku gdy test uruchamia się z zestawu i to właśnie z niego argument ma być podesłany do funkcji.

char *Function(MST_FUNCTION *info)

Funkcja testująca uruchamiana przez funkcję mst_run_test(). Powinna udowodnić poprawność testowanego kodu poprzez stosowanie makra mst_assert dla każdego testowanego wyrażenia. Dzięki temu błąd, który wystąpi podczas działania funkcji zostanie odpowiednio odnotowany w konsoli. Funkcja jako argument pobiera strukturę informacji o teście, dzięki której można pobrać zapisane w niej dane. Wystąpienie błędu w asercji podczas działania funkcji natychmiast przerywa jej działanie, zwracając treść błędu. Nazwa parametru funkcji nie może być zmieniana, zawsze musi to być info.

Przykład prostej funkcji testującej:

char *test_function( MST_FUNCTION *info )
{
    int ercode;
    mst_assert( info->Data != NULL );

    ercode = do_something_with_data( info->Data );
    mst_assert( !ercode );

    return MST_SUCCESS;
}
Parametry:
  • info – Struktura zawierająca informacje o przetwarzanym teście.
Zwraca:

Wartość NULL lub treść błędu który wystąpił w przetwarzanej asercji.

char *Name

Nazwa testu używana przy wyświetlaniu informacji o uruchomionym teście w kosnoli. Nazwa reprezentuje test podczas wyświetlania w konsoli, dzięki czemu zamiast numerków oznaczających indeksy testów wyświetlane są przypisane do nich nazwy. To pole struktury jest wymagane i nie może być oznaczone wartością NULL.

char *Desc

Opis testu używany przy wyświetlaniu informacji o uruchomionym teście w konsoli. Opis powinien być krótki i przejrzysty, dzięki czemu może pomóc w zrozumieniu na czym tak właściwie polega napisany test, bez zaglądania w kod i odczytywania komentarzy. Wartość ta nie jest wymagana i w przypadku braku opisu powinna być oznaczona wartością NULL, co spowoduje, że opis nie zostanie w ogóle wyświetlony w konsoli.

void *Data

Dodatkowe dane przekazywane do funkcji testującej wraz ze strukturą testu. Zmienna nie jest wymagana i w przypadku braku danych powinna być oznaczona wartością NULL. Podczas wykonywania zestawu testów, wartość ta w przypadku ustawienia wartości NULL jest zastępowana wartością zmiennej globalnej dla całego zestawu, ustawioną w jego strukturze.

size_t PassedAsserts

Ilość poprawnie wykonanych asercji w podpiętej do struktury funkcji testu. Zmienna resetowana jest do wartości 0 podczas startu testu i zwiększana automatycznie w trakcie wywoływania makra pozwalającego na sprawdzenie danego wyrażenia. Można dzięki temu wypisać ilość wszystkich asercji w funkcji lub tylko tych, które zostały wywołane do wystąpienia błędu. Wartość tej zmiennej nie powinna być zmieniana manualnie.

Pole to można pominąć podczas inicjalizacji struktury:

MST_FUNCTION test = { test_func, "func_name", "desc", NULL };
char *ErrorMessage

Treść błędu generowanego podczas działania testu. W trakcie błędu, dla zmiennej przydzielana jest pamięć, potrzebna do przechowania treści wiadomości. Zmienna ta jest wypełniana automatycznie przez funkcje raportujące błędy, wywoływane przez asercje. Pamięć przydzielona dla tej zmiennej nie jest zwalniana automatycznie.

MST_SUITE

Struktura odpowiedzialna za przechowywanie informacji o zestawie, zawierającym funkcje testujące. W głównej mierze struktura wykorzystywana jest w funkcji mst_run_suite(). Jednym z ważniejszych pól jest pole MST_SUITE.BreakOnError, gdzie wartość decyduje o tym, czy zbiór podczas działania zostanie przerwany po wykryciu błędu. Struktura jest przekazywana do funkcji wywoływanych przed rozpoczęciem i zaraz po zakończeniu wszystkich dostępnych w tablicy testów. Ostatni test w tablicy powinien zawierać wszystkie pola równe wartości NULL lub 0. Najlepszym sposobem jest inicjalizacja ostatniego rekordu makrem MST_LASTRECORD.

Przykład inicjalizacji struktury:

/* lista funkcji testujących */
MST_FUNCTION suite_functions[] = {
    { MST_STRINGIFY(mst_test_01), "Desc_01", NULL },
    { MST_STRINGIFY(mst_test_02), "Desc_02", NULL },
    { MST_STRINGIFY(mst_test_02), "Desc_03", NULL },
    { MST_LASTRECORD }
};

int sample_data = 5;

/* inicjalizacja struktury dla zestawu */
MST_SUITE suite_tests = {
    "Suite description",      /* Desc */
    TRUE,                     /* BreakOnError */
    setup_test_function,      /* Setup */
    teardown_test_function,   /* TearDown */
    &sample_data,             /* Data */
    suite_functions           /* Tests */
};

Powyższy kod utworzy zestaw testów, przerywanych w przypadku wystąpienia błędu. Każda struktura testu otrzyma wartość sample_data w polu odpowiedzialnym za dane. Każdy test może być zależny od danych, zmodyfikowanych w poprzednim teście, dlatego struktura posiada pole MST_SUITE.BreakOnError ustawione na wartość TRUE.

char *Desc

Opis zestawu wyświetlany przed uruchomieniem jakiegokolwiek testu. Pole to powinno opisywać w skrócie całość zestawu, choć może być jednocześnie jego nazwą. Opis wykorzystywany jest w funkcji mst_run_suite(). Pole jest wymagane, więc powinno zawierać przynajmniej informację o tym, do czego zestaw się odnosi.

bool *BreakOnError

Informacja o tym, czy zestaw po wykryciu błędu w jednej z funkcji testujących ma przerwać działanie pozostałych. Pole to jest bardzo ważne w przypadku, gdy dane wychodzące z jednej funkcji testowej, modyfikowane są w niej i przekazywane do drugiej, która operuje na modyfikacjach i nie może bez nich wykonać się prawidłowo. Gdy pole zawiera wartość TRUE i w jednej z funkcji testujących wystąpi bład w asercji, cały zestaw uruchamianych testów natychmiast zostaje przerwany, zwracając błąd. W przeciwnym wypadku funkcja nadal zwróci błąd, ale dopiero po zakończeniu wykonywania wszystkich znajdujących się w zestawie testów.

int Setup(MST_SUITE *info)

Funkcja wywoływana przed wykonaniem jakiekogolwiek testu. Może być potraktowana jako funkcja pozwalająca na przygotowanie danych do testowania. Jako argument przyjmuje strukturę z informacjami o zestawie w której znajdują się również przekazane dane globalne dla każdego testu. W przypadku gdy funkcja zwróci wartość inną niż 0, funkcje testowe nie są wywoływane. Pole nie jest wymagane i w przypadku braku funkcji należy podać wartość NULL.

Przykład prostej funkcji:

int setup_test( MST_SUITE *info )
{
    if( info->Data )
        return MSEC_INVALID_ARGUMENT;

    /* przygotuj tablicę do działania */
    info->Data = ms_array_alloc( sizeof(int), 100 );
    return info->Data
        ? MSEC_OK
        : MSEC_MEMORY_ALLOCATION;
}
Parametry:
  • info – Wskaźnik na strukturę zawierającą informacje o uruchomionym zestawie.
Zwraca:

Kod błędu który wystąpił podczas działania funkcji lub wartość 0.

void TearDown(MST_SUITE *info)

Funkcja wywoływana po wykonaniu testów zawartych w zestawie. Wywołanie tej funkcji występuje nawet gdy w trakcie testów wyktyty zostanie błąd a struktura będzie miała ustawione pole MST_SUITE.BreakOnError na wartość TRUE. Główne zastosowanie tej funkcji to zwalnianie pamięci pozostałej po wykonanych testach. Pole nie jest wymagane i w przypadku braku funkcji należy podać wartość NULL.

Przykład prostej funkcji:

struct ARRAYSET {
    MS_ARRAY a1, a2, a3, a4;
};

void teardown_test( MST_SUITE *info )
{
    struct ARRAYSET *aset = (struct ARRAYSET*)info->Data;

    if( !aset )
        return;

    ms_array_free( &aset->a1 );
    ms_array_free( &aset->a2 );
    ms_array_free( &aset->a3 );
    ms_array_free( &aset->a4 );
}
Parametry:
  • info – Wskaźnik na strukturę zawierającą informacje o uruchomionym zestawie.
void *Data

Dodatkowe dane przekazywane do struktury testu. W przypadku gdy struktura testu zawiera swoje własne dane, pole to nie jest wykorzystywane. W przeciwnym wypadku wartość pola kopiowana jest do pola MST_FUNCTION.Data, gdzie przekazywana jest wraz ze strukturą do funkcji testującej. Pole to nie jest wymagane i powinno być ustawiane na wartość NULL w przypadku gdy do funkcji nie mają być przekazywane żadne dane.

MST_FUNCTION *Tests

Tablica zawierająca testy do wykonania podczas uruchomienia zestawu. Wszystkie testy znajdujące się na liście w tym polu, zczytywane są i wykonywane przez funkcję mst_run_suite(). Testy wykonywane są w takiej kolejności w jakiej zostały podane w tablicy, aż do napotkania ostatniego rekordu, który musi być zainicjalizowany wartościami NULL lub 0 dla każdego pola struktury MST_FUNCTION. Do ułatwienia tego zadania stworzono makro o nazwie MST_LASTRECORD.

Przykład tablicy z testami przekazywanymi do zestawu:

MST_FUNCTION TestFunctions[] =
{
    { MST_STRINGIFY(mst_test_01), "Desc_01", NULL },
    { MST_STRINGIFY(mst_test_02), "Desc_02", NULL },
    { MST_STRINGIFY(mst_test_03), "Desc_03", NULL },
    { MST_STRINGIFY(mst_test_04), "Desc_04", NULL },
    { MST_STRINGIFY(mst_test_05), "Desc_05", NULL },
    { MST_STRINGIFY(mst_test_06), "Desc_06", NULL },
    { MST_STRINGIFY(mst_test_07), "Desc_07", NULL },
    { MST_STRINGIFY(mst_test_08), "Desc_08", NULL },
    { MST_STRINGIFY(mst_test_09), "Desc_09", NULL },

    /* ostatni rekord można inicjalizować w ten sposób */
    { MST_LASTRECORD },

    /* lub w ten, choć ostatnie 0 jest zbędne */
    { NULL, NULL, NULL, NULL, 0 }
};

2.2. Funkcje

int mst_run_suite(MST_SUITE *suite)

Funkcja uruchamia zestaw testów jednostkowych, przypisanych do podanej struktury w postaci tablicy. Przed uruchomieniem wyświetlana jest informacja o aktualnie działającym zestawie i wywoływana jest funkcja MST_SUITE.Setup, pozwalająca na przygotowanie danych do testów. Następnie wywoływane są po koleji wszystkie testy z tablicy przypisanej do pola MST_SUITE.Tests. W przypadku gdy któryś z nich zwróci błąd a pole MST_SUITE.BreakOnError będzie ustawione na wartość TRUE, pętla wywołująca funkcje testowe zostanie przerwana. Po zakończeniu wszystkich dostępnych w tablicy testów lub w przypadku ich przerwania, wywoływana jest funkcja zapisana w polu MST_SUITE.TearDown, pozwalająca na zwolnienie pamięci przydzielonej na dane testowe. Kod błędu zwracany przez funkcję nie jest ustalony z racji tego, iż jest on zależny w głównej mierze od funkcji mst_run_test().

Przykład użycia funkcji:

...
MST_FUNCTION tests[] = {
    { test_func1, "func_name1", "desc1", NULL },
    { test_func2, "func_name2", "desc2", NULL },
    { test_func3, "func_name3", "desc3", NULL }
};
MST_SUITE suite = {
    "Suite description",
    TRUE,
    NULL, NULL, NULL,
    tests
};
...
if( mst_run_suite(&suite) )
    return -1;
...
Parametry:
  • func – Struktura zawierająca informacje o zestawie funkcji testujących.
Zwraca:

Wartość różna od 0 w przypadku błędu lub 0.

int mst_run_test(MST_FUNCTION *info, size_t current, size_t count)

Funkcja uruchamia test jednostkowy przypisany do podanej struktury. Przed jego uruchomieniem wyświetla informacje o teście podane w strukturze. Funkcja wywoływana jest w głównej mierze wprost z funkcji mst_run_suite(), jednak może być wywoływana samodzielnie. Na uwagę zasługują ostatnie dwa parametry, reprezentujące numer aktualnego testu i ilość wszystkich testów. Liczby te wyświetlane są nad informacjami wypisywanymi ze struktury. Można je pominąć, wpisując w ich miejsce wartości 0, co spowoduje pominięcie ich w trakcie wypisywania informacji w konsoli. Funkcja w przypadku wystąpienia błędu zwróci wartość różną od 0, która będzie reprezentowała ilość znaków w zwróconej przez test wiadomości.

Przykład użycia funkcji:

...
MST_FUNCTION test1 = { test_func1, "func_name1", "desc1", NULL };
MST_FUNCTION test2 = { test_func2, "func_name2", "desc2", NULL };
...
if( mst_run_test(&test1, 0, 0) )
    return -1;
if( mst_run_test(&test2, 3, 50) )
    return -1;
...
Parametry:
  • info – Struktura zawierająca informacje o funkcji testującej.
  • current – Aktualny numer testu lub 0.
  • count – Ilość wszystkich testów lub 0.
Zwraca:

Wartość 0 lub w przypadku błędu ilość wypisanych znaków.

int mst_report_sint(MST_FUNCTION *info, const char *exp, const char *file, int line, llong a, llong b)

Tworzenie raportu o błędzie, spowodowanym przez asercje sprawdzanego wyrażenia, składającego się z dwóch liczb całkowitych. Funkcja w głównej mierze wywoływana jest automatycznie przez makro mst_assert_sint. Uruchomienie funkcji powoduje przydzielenie pamięci dla pola MST_FUNCTION.ErrorMessage i zapisanie w nim treści błędu, tworzonego z przekazanych parametrów. Przydzieloną pamięć należy zwolnić samodzielnie po wykorzystaniu danych, zapisanych w zmiennej.

Parametry:
  • info – Struktura zawierająca informacje o funkcji testującej.
  • exp – Wyrażenie w którym wystąpił błąd.
  • file – Plik w którym wystąpił błąd.
  • line – Linia w której wystąpił błąd w pliku.
  • a – Lewa strona wyrażenia, znajdująca się przed operatorem głównym.
  • b – Prawa strona wyrażenia, znajdująca się po operatorze głównym.
Zwraca:

Kod błędu lub wartość 0.

int mst_report_uint(MST_FUNCTION *info, const char *exp, const char *file, int line, ullong a, ullong b)

Tworzenie raportu o błędzie, spowodowanym przez asercje sprawdzanego wyrażenia, składającego się z dwóch liczb naturalnych. Funkcja w głównej mierze wywoływana jest automatycznie przez makro mst_assert_uint. Uruchomienie funkcji powoduje przydzielenie pamięci dla pola MST_FUNCTION.ErrorMessage i zapisanie w nim treści błędu, tworzonego z przekazanych parametrów. Przydzieloną pamięć należy zwolnić samodzielnie po wykorzystaniu danych, zapisanych w zmiennej.

Parametry:
  • info – Struktura zawierająca informacje o funkcji testującej.
  • exp – Wyrażenie w którym wystąpił błąd.
  • file – Plik w którym wystąpił błąd.
  • line – Linia w której wystąpił błąd w pliku.
  • a – Lewa strona wyrażenia, znajdująca się przed operatorem głównym.
  • b – Prawa strona wyrażenia, znajdująca się po operatorze głównym.
Zwraca:

Kod błędu lub wartość 0.

int mst_report_float(MST_FUNCTION *info, const char *exp, const char *file, int line, ldouble a, ldouble b)

Tworzenie raportu o błędzie, spowodowanym przez asercje sprawdzanego wyrażenia, składającego się z dwóch liczb rzeczywistych reprezentowanych przez zapis zmiennoprzecinkowy. Funkcja w głównej mierze wywoływana jest automatycznie przez makro mst_assert_float. Uruchomienie funkcji powoduje przydzielenie pamięci dla pola MST_FUNCTION.ErrorMessage i zapisanie w nim treści błędu, tworzonego z przekazanych parametrów. Przydzieloną pamięć należy zwolnić samodzielnie po wykorzystaniu danych, zapisanych w zmiennej. Należy pamiętać, że porównywanie liczb zmiennoprzecinkowych nie wygląda tak samo jak porównywanie liczb całkowitych.

Parametry:
  • info – Struktura zawierająca informacje o funkcji testującej.
  • exp – Wyrażenie w którym wystąpił błąd.
  • file – Plik w którym wystąpił błąd.
  • line – Linia w której wystąpił błąd w pliku.
  • a – Lewa strona wyrażenia, znajdująca się przed operatorem głównym.
  • b – Prawa strona wyrażenia, znajdująca się po operatorze głównym.
Zwraca:

Kod błędu lub wartość 0.

int mst_report(MST_FUNCTION *info, const char *exp, const char *file, int line)

Tworzenie raportu o błędzie, spowodowanym przez asercje na wyrażeniu, które posiada z prawej i lewej strony typ logiczny. Dlatego w raporcie wyświetlane jest tylko podane wyrażenie, bez informacji o wartościach stojących po prawej i lewej stronie. Funkcja w głównej mierze wywoływana jest automatycznie przez makro mst_assert. Uruchomienie funkcji powoduje przydzielenie pamięci dla pola MST_FUNCTION.ErrorMessage i zapisanie w nim treści błędu, tworzonego z przekazanych parametrów. Przydzieloną pamięć należy zwolnić samodzielnie po wykorzystaniu danych, zapisanych w zmiennej.

Parametry:
  • info – Struktura zawierająca informacje o funkcji testującej.
  • exp – Wyrażenie w którym wystąpił błąd.
  • file – Plik w którym wystąpił błąd.
  • line – Linia w której wystąpił błąd w pliku.
Zwraca:

Kod błędu lub wartość 0.

int mst_report_cs(MST_FUNCTION *info, const char *file, int line, const char *a, const char *b)

Tworzenie raportu o błędzie, spowodowanym przez porównanie dwóch przekazanych ciągów znaków, zawierających znaki składające się z jednego lub wielu bajtów. Funkcja w głównej mierze wywoływana jest automatycznie przez makra mst_assert_cs oraz mst_asser_mbs. Uruchomienie funkcji powoduje przydzielenie pamięci dla pola MST_FUNCTION.ErrorMessage i zapisanie w nim treści błędu, tworzonego z przekazanych parametrów. Przydzieloną pamięć należy zwolnić samodzielnie po wykorzystaniu danych, zapisanych w zmiennej.

Parametry:
  • info – Struktura zawierająca informacje o funkcji testującej.
  • exp – Wyrażenie w którym wystąpił błąd.
  • file – Plik w którym wystąpił błąd.
  • line – Linia w której wystąpił błąd w pliku.
  • a – Ciąg znaków znajdujący się po lewej stronie.
  • b – Ciąg znaków znajdujący się po prawej stronie.
Zwraca:

Kod błędu lub wartość 0.

int mst_report_wcs(MST_FUNCTION *info, const char *file, int line, const wchar_t *a, const wchar_t *b)

Tworzenie raportu o błędzie, spowodowanym przez porównanie dwóch przekazanych ciągów znaków, zawierających znaki o typie wchar_t o rozmiarze 2 lub 4 bajtów. Funkcja w głównej mierze wywoływana jest automatycznie przez makro mst_assert_wcs. Uruchomienie funkcji powoduje przydzielenie pamięci dla pola MST_FUNCTION.ErrorMessage i zapisanie w nim treści błędu, tworzonego z przekazanych parametrów. Przydzieloną pamięć należy zwolnić samodzielnie po wykorzystaniu danych, zapisanych w zmiennej.

Parametry:
  • info – Struktura zawierająca informacje o funkcji testującej.
  • exp – Wyrażenie w którym wystąpił błąd.
  • file – Plik w którym wystąpił błąd.
  • line – Linia w której wystąpił błąd w pliku.
  • a – Ciąg znaków znajdujący się po lewej stronie.
  • b – Ciąg znaków znajdujący się po prawej stronie.
Zwraca:

Kod błędu lub wartość 0.

2.3. Makra

void mst_prepare(MST_FUNCTION *info)

Przygotowuje strukturę danych w funkcji do użycia w makrach. Makro to należy wywołać w funkcji testującej bezpośrednio po deklaracjach wykorzystywanych zmiennych. Przy okazji sprawdza, czy struktura przekazana w parametrze jest poprawna.

Przykład użycia makra:

int test_function( MST_FUNCTION *info )
{
    int x, y, z;

    /* makro wywoływane zaraz po deklaracjach */
    mst_prepare( info );

    for( x = 5; y = 1, z = 0; z < 10; ++z )
        mst_assert( x + y == x++ + ++y - 1 );

    return MSEC_OK;
}
Parametry:
  • info – Struktura zawierająca informacje o funkcji testującej.
int mst_assert(expression exp)

Makro sprawdzające wartość wyrażenia podanego w argumencie. Makro zwiększa wartość pola MST_FUNCTION.PassedAsserts w przypadku gdy wyrażenie podane w argumencie okaże się prawdziwe. Gdy wyrażenie okaże się fałszywe, makro natychmiast zakończy funkcję w której zostało wywołane. Wywoływana funkcja raportowania powinna zwracać ilość znaków zapisanych w zmiennej MST_FUNCTION.ErrorMessage, dlatego funkcja w której makro jest wywoływane, musi zwracać typ int. Asercja ta działa dobrze w przypadku sprawdzania poprawności zmiennych o typie logicznym czy przyrównywania wartość do NULL. W pozostałych przypadkach informacje mogą się okazać zbyt mało szczegółowe. Makro wywołuje funkcje raportowania mst_report(), automatycznie uzupełniając potrzebne argumenty.

Pełny przykład użycia makra:

int test_function( MST_FUNCTION *info )
{
    bool istrue = TRUE;

    mst_prepare( info );

    /* ta asercja będzie w porządku */
    mst_assert( istrue == TRUE );

    /* ta już nie, funkcja powinna zwrócić błąd */
    mst_assert( istrue == FALSE );

    return MSEC_OK;
}

int main( int argc, char **argv )
{
    MST_FUNCTION tfunc = { MST_LASTRECORD };
    int ercode = test_function( &tfunc );

    printf( "Error code: 0x%X\nMessage:\n-----------------\n%s\n",
        ercode, tfunc.ErrorMessage );

    return 0;
}

Przykładowe wyjście:

Error code: 0x2A
Message:
-----------------
# Error in test.c on line 14
# istrue == 0
Parametry:
  • exp – Wyrażenie do sprawdzenia.
Zwraca:

Kod błędu zwracany przez funkcję raportu lub nic.

int mst_assert_sint(expression left, operator compare, expression right)

Makro sprawdzające wartość wyrażenia formułowanego z podanych argumentów. Wersja dla liczb całkowitych, pozwalająca na wyświetlenie wartości lewej i prawej strony wyrażenia. Makro zwiększa wartość pola MST_FUNCTION.PassedAsserts w przypadku gdy wyrażenie okaże się prawdziwe. W przeciwnym wypadku makro natychmiast zakończy funkcję w której zostało wywołane. Wywoływana funkcja raportowania powinna zwracać ilość znaków zapisanych w zmiennej MST_FUNCTION.ErrorMessage, dlatego funkcja w której makro jest wywoływane, musi zwracać typ int. Makro wywołuje funkcje raportowania mst_report_sint(), automatycznie uzupełniając potrzebne argumenty.

Pełny przykład użycia makra:

int test_function( MST_FUNCTION *info )
{
    mst_prepare( info );

    /* ta asercja będzie w porządku */
    mst_assert_sint( 2 + 2, ==, 4 );

    /* ta już nie, funkcja powinna zwrócić błąd */
    mst_assert_sint( 2 + 2, ==, 5 );

    return MSEC_OK;
}

int main( int argc, char **argv )
{
    MST_FUNCTION tfunc = { MST_LASTRECORD };
    int ercode = test_function( &tfunc );

    printf( "Error code: 0x%X\nMessage:\n-----------------\n%s\n",
        ercode, tfunc.ErrorMessage );

    return 0;
}

Przykładowe wyjście:

Error code: 0x42
Message:
-----------------
# Error in test.c on line 11
# 2 + 2 == 5
# Where: L = 4 and R = 5
Parametry:
  • left – Lewa strona wyrażenia.
  • compare – Operator, stojący pomiędzy prawą a lewą stroną.
  • right – Prawa strona wyrażenia.
Zwraca:

Kod błędu zwracany przez funkcję raportu lub nic.

int mst_assert_uint(expression left, operator compare, expression right)

Makro sprawdzające wartość wyrażenia formułowanego z podanych argumentów. Wersja dla liczb naturalnych, pozwalająca na wyświetlenie wartości lewej i prawej strony wyrażenia. Makro zwiększa wartość pola MST_FUNCTION.PassedAsserts w przypadku gdy wyrażenie okaże się prawdziwe. W przeciwnym wypadku makro natychmiast zakończy funkcję w której zostało wywołane. Wywoływana funkcja raportowania powinna zwracać ilość znaków zapisanych w zmiennej MST_FUNCTION.ErrorMessage, dlatego funkcja w której makro jest wywoływane, musi zwracać typ int. Makro wywołuje funkcje raportowania mst_report_uint(), automatycznie uzupełniając potrzebne argumenty.

Pełny przykład użycia makra:

int test_function( MST_FUNCTION *info )
{
    ullong max = ULLONG_MAX;

    mst_prepare( info );

    /* ta asercja będzie w porządku */
    mst_assert_uint( max - 3, ==, max - 3 );

    /* ta już nie, funkcja powinna zwrócić błąd */
    mst_assert_uint( max - 3, ==, max - 4 );

    return MSEC_OK;
}

int main( int argc, char **argv )
{
    MST_FUNCTION tfunc = { MST_LASTRECORD };
    int ercode = test_function( &tfunc );

    printf( "Error code: 0x%X\nMessage:\n-----------------\n%s\n",
        ercode, tfunc.ErrorMessage );

    return 0;
}

Przykładowe wyjście:

Error code: 0x70
Message:
-----------------
# Error in test.c on line 14
# max - 3 == max - 4
# Where: L = 18446744073709551612 and R = 18446744073709551611
Parametry:
  • left – Lewa strona wyrażenia.
  • compare – Operator, stojący pomiędzy prawą a lewą stroną.
  • right – Prawa strona wyrażenia.
Zwraca:

Kod błędu zwracany przez funkcję raportu lub nic.

int mst_assert_float(expression left, operator compare, expression right)

Makro sprawdzające wartość wyrażenia formułowanego z podanych argumentów. Wersja dla liczb zmiennoprzecinkowych, pozwalająca na wyświetlenie wartości lewej i prawej strony wyrażenia. Makro zwiększa wartość pola MST_FUNCTION.PassedAsserts w przypadku gdy wyrażenie okaże się prawdziwe. W przeciwnym wypadku makro natychmiast zakończy funkcję w której zostało wywołane. Wywoływana funkcja raportowania powinna zwracać ilość znaków zapisanych w zmiennej MST_FUNCTION.ErrorMessage, dlatego funkcja w której makro jest wywoływane, musi zwracać typ int. Makro wywołuje funkcje raportowania mst_report_float(), automatycznie uzupełniając potrzebne argumenty.

Pełny przykład użycia makra:

int test_function( MST_FUNCTION *info )
{
    float fnum = 0.2f;

    mst_prepare( info );

    /* ta asercja będzie w porządku */
    mst_assert_float( fnum, ==, 0.2f );

    /* ta już nie, funkcja powinna zwrócić błąd */
    mst_assert_float( fnum - 0.1f, ==, 0.2f );

    return MSEC_OK;
}

int main( int argc, char **argv )
{
    MST_FUNCTION tfunc = { MST_LASTRECORD };
    int ercode = test_function( &tfunc );

    printf( "Error code: 0x%X\nMessage:\n-----------------\n%s\n",
        ercode, tfunc.ErrorMessage );

    return 0;
}

Przykładowe wyjście:

Error code: 0x59
Message:
-----------------
# Error in main.c on line 14
# fnum - 0.1f == 0.2f
# Where: L = 0.100000 and R = 0.200000
Parametry:
  • left – Lewa strona wyrażenia.
  • compare – Operator, stojący pomiędzy prawą a lewą stroną.
  • right – Prawa strona wyrażenia.
Zwraca:

Kod błędu zwracany przez funkcję raportu lub nic.

int mst_assert_cs(const char *left, const char *right)

Makro pozwalające na porównanie dwóch ciągów znaków, zawierających znaki jednobajtowe. Ciągi porównywane są funkcją strcmp. Makro zwiększa wartość pola MST_FUNCTION.PassedAsserts w przypadku gdy wyrażenie okaże się prawdziwe. W przeciwnym wypadku makro natychmiast zakończy funkcję w której zostało wywołane. Wywoływana funkcja raportowania powinna zwracać ilość znaków zapisanych w zmiennej MST_FUNCTION.ErrorMessage, dlatego funkcja w której makro jest wywoływane, musi zwracać typ int. Makro wywołuje funkcje raportowania mst_report_cs(), automatycznie uzupełniając potrzebne argumenty.

Pełny przykład użycia makra:

int test_function( MST_FUNCTION *info )
{
    const char *str = "The quick brown fox jumps over the lazy dog.";
    mst_prepare( info );

    /* ta asercja będzie w porządku */
    mst_assert_cs(
        str,
        "The quick brown fox jumps over the lazy dog."
    );

    /* ta już nie, funkcja powinna zwrócić błąd */
    mst_assert_cs(
        str,
        "The quick brown fox jumps over the lazy cat."
    );

    return MSEC_OK;
}

int main( int argc, char **argv )
{
    MST_FUNCTION tfunc = { MST_LASTRECORD };
    int ercode = test_function( &tfunc );

    printf( "Error code: 0x%X\nMessage:\n-----------------\n%s\n",
        ercode, tfunc.ErrorMessage );

    return 0;
}

Przykładowe wyjście:

Error code: 0xA7
Message:
-----------------
# Error in main.c on line 19
# Function strcmp( L, R ) failed...
# L = The quick brown fox jumps over the lazy dog.
# R = The quick brown fox jumps over the lazy cat.
Parametry:
  • left – Ciąg znaków po lewej stronie.
  • right – Ciąg znaków po prawej stronie.
Zwraca:

Kod błędu zwracany przez funkcję raportu lub nic.

int mst_assert_mbs(const char *left, const char *right)

Makro pozwalające na porównanie dwóch ciągów znaków, zawierających znaki jedno lub kilku bajtowe. Na chwilę obecną makro to jest tylko aliasem makra o nazwie mst_assert_cs. Warto jednak używać go do sprawdzania ciągów wielobajtowych znaków.

Parametry:
  • left – Ciąg znaków po lewej stronie.
  • right – Ciąg znaków po prawej stronie.
Zwraca:

Kod błędu zwracany przez funkcję raportu lub nic.

int mst_assert_wcs(const wchar_t *left, const wchar_t *right)

Makro pozwalające na porównanie dwóch ciągów znaków, zawierających znaki o typie wchar_t. Ciągi porównywane są funkcją wcscmp. Makro zwiększa wartość pola MST_FUNCTION.PassedAsserts w przypadku gdy wyrażenie okaże się prawdziwe. W przeciwnym wypadku makro natychmiast zakończy funkcję w której zostało wywołane. Wywoływana funkcja raportowania powinna zwracać ilość znaków zapisanych w zmiennej MST_FUNCTION.ErrorMessage, dlatego funkcja w której makro jest wywoływane, musi zwracać typ int. Makro wywołuje funkcje raportowania mst_report_wcs(), automatycznie uzupełniając potrzebne argumenty.

Pełny przykład użycia makra:

int test_function( MST_FUNCTION *info )
{
    const wchar_t *str =
        L"Чушь: гид вёз кэб цапф, юный жмот съел хрящ.";
    mst_prepare( info );

    /* ta asercja będzie w porządku */
    mst_assert_wcs(
        str,
        L"Чушь: гид вёз кэб цапф, юный жмот съел хрящ."
    );

    setlocale( LC_ALL, "" );

    /* ta już nie, funkcja powinna zwrócić błąd */
    mst_assert_wcs(
        str,
        L"Чушь: гид вёз кэб цапф, жмот съел хрящ."
    );

    return MSEC_OK;
}

int main( int argc, char **argv )
{
    MST_FUNCTION tfunc = { MST_LASTRECORD };
    int ercode = test_function( &tfunc );

    printf( "Error code: 0x%X\nMessage:\n-----------------\n%s\n",
        ercode, tfunc.ErrorMessage );

    return 0;
}

Przykładowe wyjście:

Error code: 0xE0
Message:
-----------------
# Error in main.c on line 23
# Function wcscmp( L, R ) failed...
# L = Чушь: гид вёз кэб цапф, юный жмот съел хрящ.
# R = Чушь: гид вёз кэб цапф, жмот съел хрящ.
Parametry:
  • left – Ciąg znaków po lewej stronie.
  • right – Ciąg znaków po prawej stronie.
Zwraca:

Kod błędu zwracany przez funkcję raportu lub nic.

part MST_STRINGIFY(literal func)

Makro wstawiające w miejsce wystąpienia wskazanie na funkcję oraz jej nazwę w postaci ciągu znaków. Makro jest częścią inicjalizacji struktury MST_FUNCTION i tylko wtedy powinno być stosowane. Jego użycie upraszcza nieco kod w przypadku funkcji o długich nazwach.

Przykład użycia makra:

/* ta inicjalizacja */
MST_FUNCTION test1 = { MST_STRINGIFY(some_test_function), "desc", NULL };

/* jest równoznaczna z tą */
MST_FUNCTION test2 = { some_test_function, "some_test_function", "desc", NULL };
Parametry:
  • func – Nazwa funkcji zamieniana na ciąg znaków.
Zwraca:

Dwa pierwsze pola podczas inicjalizacji struktury.

MST_LASTRECORD

Makro pozwalające na szybką inicjalizację ostatniego rekordu testu dla struktury zestawu. Wystarczy użyć tego makra, zamiast wpisywać dla każdego pola wartość NULL. Makro przeznaczone tylko do inicjalizacji struktury MST_FUNCTION wewnątrz MST_SUITE.

Przykład użycia makra:

/* ta inicjalizacja */
MST_FUNCTION test1 = { MST_LASTRECORD };

/* jest równoznaczna z tą */
MST_FUNCTION test2 = { NULL, NULL, NULL, NULL, 0, NULL };

2.4. Pokrycie kodu

Pokrycie kodu z punktu widzenia tworzenia testów jednostkowych jest jedną z ważniejszych kwestii, które należy poruszyć. Tworzenie statystyk pokrycia kodu, pozwala na wykrycie miejsc, do których program nie zdołał dojść w trakcie działania. Metoda ta jest więc stosowana do wykrywania zarówno starego kodu, który od dawna już za nic nie odpowiada, zajmując niepotrzebnie miejsce w plikach, jak i również miejsc, do których napisany test jednostkowy nie zdołał dojść. Dzięki temu można dopisać kod uwzlgędniający przypadek, który przechodzi przez daną część kodu testowanego. Do tworzenia informacji o pokryciu kodu potrzebne jest specjalne narzędzie, przeznaczone do tego celu. Różne systemy oferują różne narzędzia, które są uzależnione od użytego kompilatora.

2.4.1. GCOV

Systemy z rodziny Unix (którym jest de facto Linux) udostępniają narzędzie gcov, działające wraz z kompilatorem gcc. Aby całość mogła współgrać ze sobą, program należy skompilować wraz z odpowiednimi parametrami, które umożliwią dokładne wykrycie linii, które są aktywowane podczas działania programu.

Poniższy wycinek kodu przedstawia przykład kompilacje testu w gcc:

gcc "source.c" "test_source.c" -o "program.out"
    -g                      # włącza debugowanie
    -O0                     # wyłącza optymalizację kodu przez kompilator
    -Wall                   # włącza raportowanie wszystkich błędów
    -fprofile-arcs          # dodaje możliwość analizy kodu
    -ftest-coverage         # tworzy pliki potrzebne przez gcov

Jak można zauważyć, kompilator, otrzymując odpowiednie argumenty, pozwala przygotować kompilowany program do użycia gcov. Po poprawnej kompilacji dla każdego pliku źródłowego utworzone zostaną pliki o rozszerzeniu .gcda, które będą potrzebne podczas uruchamiania narzędzia gcov:

./program.out
gcov "source.c" "test_source.c"

Przed uruchomieniem narzędzia gcov, należy uruchomić skompilowany przed chwilą program. Spowoduje to wygenerowanie danych, które gcov będzie w stanie przetworzyć i wygenerować odpowiedni raport. Uruchomienie polecenia gcov wymaga podania plików dla których statystyki będą utworzone. Po uruchomieniu narzędzia w konsoli wyświetlą się informacje o pokryciu kodu:

Checking code coverage...
File 'source.c'
Lines executed:95.14% of 247
Creating 'array.c.gcov'

File 'test_source.c'
Lines executed:99.80% of 494
Creating 'array_test.c.gcov'

Dane przedstawiają procent wykonania kodu w pliku. Plik wygenerowany przez narzędzie o rozszerzeniu .gcov zawiera informacje o przebiegu programu. Precyzując, można tam znaleźć statystyki na temat tego, ile razy dana linia programu została wykonana, oraz które z linii nie zostały wykonane w uruchomionej instancji aplikacji. Przykład zawartości pliku prosto ze źródeł testowych modułu tablicy dynamicznej:

    -:  312:    /* sprawdź czy nowy element się zmieści */
    9:  313:    if( array->Length + size > array->Capacity )
    -:  314:    {
    -:  315:        int ercode;
    3:  316:        if( (ercode = ms_array_realloc_min(array, array->Length + size)) )
#####:  317:            return ercode;
    -:  318:    }
    6:  319:    else if( !size )
    1:  320:        SETERRNOANDRETURN( MSEC_INVALID_ARGUMENT );

Jak można zauważyć, linie które nie zostały wykonane, oznaczone są kratkami. Linie zawierające znak minusa to linie nieistotne dla przebiegu programu. Pozostałe zawierają liczbę, która przedstawia ile razy program przeszedł przed dane miejsce.

2.4.2. VSPerf

Program Microsoft Visual Studio zawiera narzędzia do generowania statystyk pokrycia kodu. Tutaj jest jednak haczyk, gdyż choć każda wersja potrafi je generować, to już nie każda potrafi je otworzyć. Odczytanie wygenerowanych statystyk wiąże się z zainstalowaniem odpowiedniej wersji produktu, posiadającego w swym zbiorze bibliotekę Microsoft.VisualStudio.Coverage.Analysis. Aktualnie, wersje zawierające ten moduł, oznaczone są nazwą Enterprise. Oczywiście w przypadku systemu Windows zawsze można użyć metody, opisanej w rozdziale GCOV po wcześniejszym zainstalowaniu środowiska MinGW lub dużo prostrzej w użyciu metody z rozdziału OpenCppCoverage.

Zestaw narzędzi VSPerf został utworzony na potrzeby zbierania danych o wydajności aplikacji. Można dzięki nim wygenerować również dane dotyczące pokrycia kodu przez program. Aby to zrobić, należy oczywiście wcześniej odpowiednio skompilować program. Wszystkie funkcje zamieszczone poniżej należy wywoływać w konsoli zawierającej ustawione ścieżki w zmiennej środowiskowej. Skrypt Visual Studio Command Prompt dołączony do menu podczas instalacji Visual Studio ustawia je automatycznie.

Poniżej przedstawiony został przykład kompilacji testu w cl:

cl "source.c" "test_source.c" /Fe"program.exe"
    /Zi                     # włącza debugowanie
    /Od                     # wyłącza optymalizację kodu przez kompilator
    /Wall                   # włącza raportowanie wszystkich błędów
    /link
    /Profile                # dodaje możliwość analizy kodu
    /OUT:NOREF              # pozostawia funkcje, które nie są w programie

Tak skompilowany program powinien dać się uruchomić. Wcześniej jednak, należy uruchomić narzędzie do instrumentacji plików binarnych, VSInstr, po czym użyć narzędzia VSPerfMon. Kolejność wywoływanych poleceń jest następująca:

VSInstr /coverage "program.exe"
start VSPerfMon /coverage /output:"program.coverage"
program.exe
VSPerfCmd /shutdown

Ta sekwencja instrukcji otwiera nowe okno linii poleceń. Sytuacja ta jest uciążliwa z punktu widzenia przetwarzania instrukcji sekwencyjnie w plikach skryptowych, gdyż program wykona się zanim narzędzie zostanie przygotowane do działania. Aby temu zapobiec, zamiast VSPerfMon można użyć VSPerfCMD:

VSPerfCmd /start:coverage /output:"program.coverage"

Uruchomienie powyższej sekwencji z użyciem narzędzia VSPerfMon lub VSPerfCMD utworzy plik o rozszerzeniu .coverage, który należy uruchomić w programie Visual Studio. To jest właśnie ten moment w którym do uruchomienia pliku i wyświetlenia wyników należy posiadać odpowiednią wersję aplikacji.

2.4.3. OpenCppCoverage

Ta darmowa aplikacja konsolowa potrafi w pełni zastąpić narzędzie VSPerfMon, generując statystyki pokrycia kodu do plików o rozszerzeniu .html. Program może również eksportować dane do plików .xml w takim układzie w jakim tworzy je aplikacja Cobertura. Możliwy jest również eksport do plików binarnych, które mogą być później łączone przez program w jeden plik wynikowy zawierający wszystkie dostępne w nich statystyki. Tworzenie i łączenie plików binarnych używane jest przez bibliotekę Moss, dzięki czemu wszystkie testowane osobno moduły zostają połączone w jeden plik, zawierający statystyki pokrycia kodu dla całej biblioteki.

Przed użyciem programu z linii poleceń, należy dodać ścieżkę aplikacji do zmiennej środowiskowej PATH. Można to oczywiście ominąć i zamiast samej nazwy programu podawać jego pełną ścieżkę podczas wywoływania. Program wymaga, aby aplikacja, do której generowane będą statystyki pokrycia kodu, była już skompilowana. Przykład kompilacji z użyciem kompilatora cl opisany został w rozdziale VSPerf.

Aplikacja w trakcie działania wykorzystuje plików .pdb, generowane przez kompilator cl ustawiony na kompilację w trybie odpluskiwania. Jest to naprawdę dobry odpowiednik narzędzia gcov dla kompilatora cl.

Program wywołać można w następujący sposób:

OpenCppCoverage --modules "directory" --sources "directory"
    --export_type html:program    # typ eksportu, tutaj HTML
    -- "program.exe"              # nazwa programu do uruchomienia

Powyższa komenda wpisana w linii poleceń zapisze do folderu o nazwie program statystyki pokrycia kodu w formacie .html dla aplikacji o nazwie program.exe. Bardzo ważne są tutaj parametry --modules oraz --sources, które pozwalają na przetwarzanie tylko plików należących do programu. Dotyczy to zarówno plików .pdb jak i plików źródłowych programu.

Przykład łączenia statystyk pokrycia kodu generowanych przez aplikację:

# pierwszy moduł
OpenCppCoverage --modules "directory" --sources "directory"
    --export_type binary:module1.bin
    -- "module1.exe"

# drugi moduł
OpenCppCoverage --modules "directory" --sources "directory"
    --export_type binary:module2.bin
    -- "module2.exe"

# łączenie plików binarnych
OpenCppCoverage
    --input_coverage "module1.bin"
    --input_coverage "module2.bin"
    --export_type html:module

Dzięki temu zabiegowi, program wygeneruje jedną stronę zawierającą statystyki pokrycia kodu dla wszystkich modułów. Prezentacja pokrycia kodu zapisana w plikach .html, przedstawia odpowiednio pokolorowane linie. Te, które nie zostały wykorzystane w trakcie działania programu oznaczone są na czerwono, pozostałe zaś na zielono.

2.5. Wycieki pamięci

Jedną z najważniejszych kwestii w trakcie tworzenia oprogramowania jest wykrycie wszelkich wycieków pamięci. Tworzą się one w przypadku, gdy ilość zwalnianej pamięci jest mniejsza niż ilość pamięci przydzielanej. Język C jest jednym z języków, w których zwalnianie pamięci nie jest wykonywane automatycznie. Każda funkcja przydzielająca zasoby musi posiadać odpowiednik, który te zasoby zwalnia. Wycieki pamięci są jednymi z najgorszych błędów do wykrycia przez programistę, dlatego do ich wyszukiwania powstały odpowiednie programy.

2.5.1. Valgrind

Podstawowym narzędziem do sprawdzania wycieków pamięci w systemach z rodziny Linux, jest program o nazwie Valgrind, choć oczywiście jest możliwość uruchomienia go na systemie Windows. Przed użyciem narzędzia Valgrind, należy skompilować w trybie odpluskiwania program, który będzie sprawdzany. Kompilację można wykonać zgodnie ze wskazówkami podanymi w rozdziałach GCOV oraz VSPerf.

Uruchomienie narzędzia sprowadza się do podania jego nazwy wraz z nazwą programu:

valgrind --leak-check=yes ./test

Po nazwie programu, w tym wypadku ./test, można podać dodatkowo argumenty przekazywane bezpośrednio do programu. W zasadzie to wszystko, jeżeli chodzi o sprawdzanie wycieków pamięci narzędziem Valgrind. Szczegóły dotyczące uruchamiania programu można znaleźć na stronie producenta.

Zakończenie programu powinien wieńczyć następujący komunikat:

==3168== HEAP SUMMARY:
==3168==     in use at exit: 0 bytes in 0 blocks
==3168==   total heap usage: 41 allocs, 41 frees, 10,772 bytes allocated
==3168==
==3168== All heap blocks were freed -- no leaks are possible
==3168==
==3168== For counts of detected and suppressed errors, rerun with: -v
==3168== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

Można z niego odczytać, że aplikacja nie ma żadnych wycieków pamięci, więc wszystko jest w porządku. W przypadku gdyby wycieki pamięci się pojawiły, to w powyższym komunikacie wypisane będą wszystkie błędy, które Valgrind zdołał wychwycić. Oczywiście możliwe są też fałszywe alarmy (false positives), ale lepiej dmuchać na zimne.

2.5.2. Dr. Memory

Bardzo rozbudowanym i zarazem prostym w obsłudze narzędziem do wykrywania wycieków pamięci jest Dr. Memory, działający zarówno na systemie Microsoft Windows jak i na systemach z rodziny Linux. Aby zaprzęgnąć narzędzie do działania, należy skompilować program w trybie odpluskiwania zgodnie z instrukcjami podanymi w rozdziałach GCOV oraz VSPerf. Dzięki temu Dr. Memory będzie mógł wykryć potencjalne błędy związane z zarządzaniem pamięcią.

Uruchomienie narzędzie jest banalnie proste:

drmemory -- test.exe

Gdzie test.exe to nazwa testowanego programu. Oczywiście po jego nazwie można podać dodatkowe argumenty, które zostaną przekazane bezpośrednio do niego. W zasadzie to tyle, jeżeli chodzi o wykrywanie wycieków pamięci narzędziem Dr. Memory. Szczegóły dotyczące uruchamiania programu można znaleźć na stronie producenta.

Zakończenie programu powinno wyglądać mniej więcej tak:

NO ERRORS FOUND:
      0 unique,     0 total unaddressable access(es)
      0 unique,     0 total uninitialized access(es)
      0 unique,     0 total invalid heap argument(s)
      0 unique,     0 total GDI usage error(s)
      0 unique,     0 total handle leak(s)
      0 unique,     0 total warning(s)
      0 unique,     0 total,      0 byte(s) of leak(s)
      0 unique,     0 total,      0 byte(s) of possible leak(s)
ERRORS IGNORED:
     21 potential error(s) (suspected false positives)
         (details: %APPDATA%\Dr. Memory\DrMemory-array.exe.6304.000\potential_errors.txt)
    119 unique,   239 total,  39196 byte(s) of still-reachable allocation(s)
         (re-run with "-show_reachable" for details)
Details: %APPDATA%\Dr. Memory\DrMemory-array.exe.6304.000\results.txt

Jak można zauważyć powyżej, program został wykonany poprawnie i nie posiada żadnych wycieków pamięci. W przypadku gdy podczas działania, program wychwyci jiekolwiek błędy, to zostaną one wypisane w powyższym komunikacie w konsoli. Narzędzie Dr. Memory wykrywa również fałszywe alarmy, te, należące do bibliotek systemowych lub używanych w programie bibliotek wstawiane są od razu w sekcję ERRORS IGNORED.

2.6. Testy biblioteki Moss

Biblioteka Moss posiada zaprogramowane testy, oparte na module MSTest. Każdy moduł posiada swój test, dzięki czemu można je testować osobno lub wszystkie na raz. Uruchomienie któregokolwiek z utworzonych testów jest banalnie proste. W folderze tst/run znajdują się skrypty odpowiadające za kompilacje testu i uruchomienie narzędzia pozwalającego na wyświetlenie statystyk pokrycia kodu. Skrypty utworzone zsotały z myślą o dwóch kompilatorach, gcc oraz cl. Dla systemu Windows są to pliki o rozszerzeniu .bat zaś dla systemu Linux, pliki o rozszerzeniu .sh.

Aby uruchomić test dla wybranego modułu (tutaj Array), wystarczy wpisać:

./array.sh   # dla systemu Linux
array.bat    # dla systemu Windows

Można również uruchomić wszystkie testy po wpisaniu:

./all.sh     # dla systemu Linux
all.bat      # dla systemu Windows

Po uruchomieniu testu lub wszystkich testów na raz, wyświetlone zostanie pokrycie kodu. Błąd w jednym teście przerywa wykonywanie pozostałych, zarówno w wybranym zestawie dla modułu jak i również podczas uruchamiania testów dla wszystkich modułów. Tak więc w przypadku uruchomienia wszystkich testów, podczas wystąpienia błędu w pierwszym module, pozostałe nie będą testowane do momentu naprawienia wykrytego błędu. Testy nie sprawdzają programu pod kątem wycieków pamięci.

Dane generowane przez skrypty zapisywane są do plików w folderze tst/gen. Po uruchomieniu skryptu to właśnie tam znajdą się pliki zawierające informacje o pokryciu kodu oraz skompilowane pliki zawierające testy, które można uruchomić samodzielnie. Dzięki temu każda zmiana w module może być przetestowana pod kątem poprawności z zapisanym w dokumentacji standardem.