Rozpoczęcie pracy z CppUTest

Ptak

14 maj 2017

Email

1 min read

Rozpoczęcie pracy z CppUTest

Kluczowe Wnioski

    • CppUTest to lekki, aktywnie utrzymywany framework testowy w stylu xUnit dla C/C++, z warstwą integracji C, która działa bezproblemowo nawet w kodzie z przewagą C.

    • Możesz go zainstalować za pomocą menedżerów pakietów (dystrybucje Linuxa, Homebrew) lub sklonować repozytorium GitHub.

    • Minimalna konfiguracja składa się z:

      • katalogu produkcyjnego src/,

      • katalogu testowego t/,

      • uruchamiania testów (CommandLineTestRunner), oraz

      • modułów testowych używających bloków TEST_GROUP i TEST().

    • CppUTest zapewnia pomocniczy MakefileWorker.mk, który upraszcza budowanie testów, łączenie bibliotek i obsługę flag.

    • Wykrywanie wycieków pamięci jest domyślnie włączone przez nadpisywanie malloc/free, co pozwala na wychwycenie wycieków w testowanym kodzie źródłowym.

    • Pokrycie kodu za pomocą gcov łatwo się integruje przez włączenie CPPUTEST_USE_GCOV=Y, co skutkuje pełnymi raportami pokrycia i podsumowaniami HTML.

    • Framework obejmuje zaawansowane funkcje: mocking, pluginy, skrypty pomocnicze oraz bezpośrednią interoperacyjność z C — przydatne w złożonych korporacyjnych bazach kodu.

Q&A Highlights

  • Co to jest CppUTest i dlaczego z niego korzystać?

    To solidne, xUnit-style framework testowania dla C/C++ z czystym API, wbudowanymi makrami assert, wykrywaniem wycieków i aktywnym rozwojem — idealne dla systemów legacy lub nowoczesnych.

  • Jak skonstruować podstawowy projekt za pomocą CppUTest?

    src/
      code/
        code.cpp
        code.h
      main.cpp
    t/
      main.cpp (test runner)
      test.cpp (test suite)
  • Jak uruchomić wszystkie testy?

    Tester używa:

    return CommandLineTestRunner::RunAllTests(ac, av);
  • Jak tworzyć testy bez ręcznej konfiguracji opcji kompilatora?

    Użyj MakefileWorker.mk z CppUTest, który automatycznie obsługuje flagi, linkowanie i wykonanie testów.

  • Czy CppUTest może automatycznie wykrywać wycieki pamięci?

    Tak. Nadpisuje malloc/free podczas budowy testów, raportując:

    • który test wyciekł,

    • gdzie to się wydarzyło,

    • rozmiar wycieku i zawartość pamięci.

    Przykład błędnego wyniku:

    Memory leak(s) found.
    Allocated at: code.c line 6
    Leak size: 1
  • Jak wygenerować code coverage?

    1. Włącz: CPPUTEST_USE_GCOV=Y

    2. Upewnij się, że filterGcov.sh jest dostępny w $(CPPUTEST_HOME)/scripts/.

    3. Uruchom: make gcov

      To generuje pliki .gcov, podsumowania tekstowe i raporty pokrycia HTML.

  • Co jeszcze może zrobić CppUTest poza podstawowym testowaniem?

    • framework do symulacji

    • system wtyczek

    • skrypty automatyzacji pomocniczej

    • natywna integracja C

    • rozległe makra asercji

  • Dla kogo CppUTest jest najbardziej odpowiedni?

    Zespoły pracujące z systemami wbudowanymi, platformami C, usługami C++ lub dowolnym środowiskiem, w którym niezawodność i bezpieczeństwo pamięci muszą być ciągle weryfikowane.

W SparkPost poświęcamy dużo czasu i wysiłku na testowanie naszego kodu. Nasza platforma jest napisana w C, a ostatnio badałem integrację z frameworkiem do testowania jednostkowego o nazwie „CppUTest”, który zapewnia testowanie w stylu xUnit dla C/C++. Ten framework jest solidny, bogaty w funkcje i znajduje się w fazie aktywnego rozwoju, co czyni go doskonałym wyborem. Zapewnia również warstwę integracji C, co ułatwia korzystanie z naszego kodu platformy C, mimo że większość frameworka jest w C++. Ten samouczek obejmuje, jak rozpocząć pracę z CppUTest w swoich własnych projektach.

Pobieranie CppUTest

Strona projektu CppUTest jest dostępna na oficjalnej stronie, a repozytorium znajduje się na github. Jest również zawarta w repozytoriach zarządzania pakietami dla wielu dystrybucji linux, a także homebrew na Mac OS. Przykłady, które dalej omówimy, były wykonane na Mac OS X, ale pochodzą z kodu napisanego dla Red Hat, systemu operacyjnego, na którym działa nasza platforma.

Podstawy są dobrze udokumentowane na stronie głównej CppUTest. Przejdziemy przez to pobieżnie i zajmiemy się niektórymi bardziej interesującymi funkcjami.

Laying the Foundation

Po pierwsze, napiszmy trochę kodu!

Nasz projekt testowy będzie miał plik ‘main’ i będzie zawierał bibliotekę użytkową o nazwie ‘code’. Biblioteka ta dostarczy prostą funkcję zwracającą 1 (na razie). Pliki będą zorganizowane w następujący sposób:

├── src
├── code
├── code.cpp
└── code.h
└── main.cpp
└── t
    ├── main.cpp
    └── test.cpp

Zacznijmy od napisania plików src/

// src/main.cpp
#include <stdlib.h>
#include <stdio.h>
#include "code.h"
int main(void) {
    test_func();
    printf("hello world!\n");
    exit(0);
}


// src/code/code.cpp
#include <stdlib.h>
#include "code.h"
int test_func() {
    return 1;
}


// src/code/code.h
#ifndef __code_h__
#define __code_h__
int test_func();
#endif

Teraz przejdźmy do testów, które będą znajdować się w katalogu t/. Pierwszą rzeczą do zrobienia jest skonfigurowanie test runnera, który uruchomi nasze pliki testowe. Jest to również funkcja ‘main’  która zostanie wykonana po skompilowaniu tego wszystkiego:

// t/main.cpp
#include "CppUTest/CommandLineTestRunner.h"
int main(int ac, char** av) {
return CommandLineTestRunner::RunAllTests(ac, av);
}

Teraz możemy napisać nasz pierwszy moduł testowy:

// t/test.cpp
#include "CppUTest/TestHarness.h"
#include "code.h"
TEST_GROUP(AwesomeExamples)
{
};
TEST(AwesomeExamples, FirstExample)
{
    int x = test_func();
    CHECK_EQUAL(1, x);
}

Następnie musimy napisać makefiles. Potrzebujemy dwóch: jednego dla plików projektu pod src/ i jednego dla testów.

Po pierwsze, napiszmy trochę kodu!

Nasz projekt testowy będzie miał plik ‘main’ i będzie zawierał bibliotekę użytkową o nazwie ‘code’. Biblioteka ta dostarczy prostą funkcję zwracającą 1 (na razie). Pliki będą zorganizowane w następujący sposób:

├── src
├── code
├── code.cpp
└── code.h
└── main.cpp
└── t
    ├── main.cpp
    └── test.cpp

Zacznijmy od napisania plików src/

// src/main.cpp
#include <stdlib.h>
#include <stdio.h>
#include "code.h"
int main(void) {
    test_func();
    printf("hello world!\n");
    exit(0);
}


// src/code/code.cpp
#include <stdlib.h>
#include "code.h"
int test_func() {
    return 1;
}


// src/code/code.h
#ifndef __code_h__
#define __code_h__
int test_func();
#endif

Teraz przejdźmy do testów, które będą znajdować się w katalogu t/. Pierwszą rzeczą do zrobienia jest skonfigurowanie test runnera, który uruchomi nasze pliki testowe. Jest to również funkcja ‘main’  która zostanie wykonana po skompilowaniu tego wszystkiego:

// t/main.cpp
#include "CppUTest/CommandLineTestRunner.h"
int main(int ac, char** av) {
return CommandLineTestRunner::RunAllTests(ac, av);
}

Teraz możemy napisać nasz pierwszy moduł testowy:

// t/test.cpp
#include "CppUTest/TestHarness.h"
#include "code.h"
TEST_GROUP(AwesomeExamples)
{
};
TEST(AwesomeExamples, FirstExample)
{
    int x = test_func();
    CHECK_EQUAL(1, x);
}

Następnie musimy napisać makefiles. Potrzebujemy dwóch: jednego dla plików projektu pod src/ i jednego dla testów.

Po pierwsze, napiszmy trochę kodu!

Nasz projekt testowy będzie miał plik ‘main’ i będzie zawierał bibliotekę użytkową o nazwie ‘code’. Biblioteka ta dostarczy prostą funkcję zwracającą 1 (na razie). Pliki będą zorganizowane w następujący sposób:

├── src
├── code
├── code.cpp
└── code.h
└── main.cpp
└── t
    ├── main.cpp
    └── test.cpp

Zacznijmy od napisania plików src/

// src/main.cpp
#include <stdlib.h>
#include <stdio.h>
#include "code.h"
int main(void) {
    test_func();
    printf("hello world!\n");
    exit(0);
}


// src/code/code.cpp
#include <stdlib.h>
#include "code.h"
int test_func() {
    return 1;
}


// src/code/code.h
#ifndef __code_h__
#define __code_h__
int test_func();
#endif

Teraz przejdźmy do testów, które będą znajdować się w katalogu t/. Pierwszą rzeczą do zrobienia jest skonfigurowanie test runnera, który uruchomi nasze pliki testowe. Jest to również funkcja ‘main’  która zostanie wykonana po skompilowaniu tego wszystkiego:

// t/main.cpp
#include "CppUTest/CommandLineTestRunner.h"
int main(int ac, char** av) {
return CommandLineTestRunner::RunAllTests(ac, av);
}

Teraz możemy napisać nasz pierwszy moduł testowy:

// t/test.cpp
#include "CppUTest/TestHarness.h"
#include "code.h"
TEST_GROUP(AwesomeExamples)
{
};
TEST(AwesomeExamples, FirstExample)
{
    int x = test_func();
    CHECK_EQUAL(1, x);
}

Następnie musimy napisać makefiles. Potrzebujemy dwóch: jednego dla plików projektu pod src/ i jednego dla testów.

Projekt Makefile

Plik makefile projektu będzie na tym samym poziomie co katalogi ‘src’ i ‘t’ w katalogu głównym projektu. Powinno to wyglądać następująco:

# Makefile

SRC_DIR = ./src
CODE_DIR = $(SRC_DIR)/code
OUT = example
TEST_DIR = t

test:
make -C $(TEST_DIR)

test_clean:
make -C $(TEST_DIR) clean

code.o:
gcc -c -I$(CODE_DIR) $(CODE_DIR)/code.cpp -o $(CODE_DIR)/code.o

main: code.o
gcc -I$(CODE_DIR) $(CODE_DIR)/code.o $(SRC_DIR)/main.cpp -o $(OUT)

all: test main

clean: test_clean
rm $(SRC_DIR)/*.o $(CODE_DIR)/*.o $(OUT)

Zanotuj, że używa to ‘make -C’  dla celów testowych – co oznacza, że ponownie wywoła ‘make’  używając makefile w katalogu testowym.

Na tym etapie możemy skompilować kod ‘src’ za pomocą makefile i zobaczyć, że działa:

[]$ make main
gcc -c -I./src/code ./src/code/code.cpp -o ./src/code/code.o
gcc -I./src/code ./src/code/code.o ./src/main.cpp -o example
[]$ ./example
hello world

Testy Makefile

W przypadku testów rzecz jest trochę bardziej skomplikowana, ponieważ musimy prawidłowo załadować i zintegrować z biblioteką CppUTest.

Repozytorium CppUTest dostarcza plik o nazwie „MakefileWorker.mk”. Zapewnia on wiele funkcji, które sprawiają, że budowanie z CppUTest jest proste. Plik znajduje się w katalogu „build” w repozytorium git. W tym samouczku założymy, że został skopiowany do katalogu ‘t/’. Można go użyć w następujący sposób:

# we don’t want to use relative paths, so we set these variables
PROJECT_DIR=/path/to/project
SRC_DIR=$(PROJECT_DIR)/src
TEST_DIR=$(PROJECT_DIR)/t

# specify where the source code and includes are located
INCLUDE_DIRS=$(SRC_DIR)/code
SRC_DIRS=$(SRC_DIR)/code

# specify where the test code is located
TEST_SRC_DIRS=$(TEST_DIR)

# what to call the test binary
TEST_TARGET=example

# where the cpputest library is located
CPPUTEST_HOME=/usr/local

# run MakefileWorker.mk with the variables defined here
include MakefileWorker.mk

Zauważ, że CPPUTEST_HOME musi być ustawiony na miejsce, gdzie zainstalowano CppUTest. Jeśli zainstalowałeś pakiet dystrybucyjny, zazwyczaj znajduje się to w /usr/local na systemie linux/mac. Jeśli sprawdziłeś repozytorium samodzielnie, to tam, gdzie jest ten checkout.

Wszystkie te opcje są udokumentowane w MakefileWorker.mk.

MakefileWorker.mk dodaje również kilka celów makefile, w tym następujące:

  1. all – buduje testy wskazane przez makefile

  2. clean – usuwa wszystkie obiekty i pliki gcov wygenerowane dla testów

  3. realclean – usuwa wszystkie pliki obiektowe i gcov w całym drzewie katalogów

  4. flags – wyświetla wszystkie skonfigurowane flagi używane do kompilacji testów

  5. debug – wyświetla wszystkie pliki źródłowe, obiekty, zależności i ‘rzeczy do czyszczenia’

Code Coverage

Testowanie jednostkowe nie byłoby kompletne bez raportu pokrycia. Narzędziem pierwszego wyboru dla projektów korzystających z gcc jest gcov, dostępne w standardowym zestawie narzędzi gcc. Cpputest łatwo integruje się z gcov, wystarczy dodać tę linię do pliku makefile:

CPPUTEST_USE_GCOV=Y

Potem musimy upewnić się, że skrypt filterGcov.sh z tego repozytorium znajduje się w '/scripts/filterGcov.sh' względem lokalizacji, w której ustawiłeś ‘CPPUTEST_HOME’. Musi on także mieć uprawnienia do wykonania.

W przykładowym pliku Makefile, byłby deployowany do '/usr/local/scripts/filterGcov.sh'. Jeśli uruchamiasz CppUTest z repozytorium, wszystko powinno działać bez modyfikacji.

Z tym w miejscu, możesz po prostu uruchomić ‘make gcov’ i analiza zostanie wygenerowana dla Ciebie. W naszym przypadku będziemy musieli ‘make -B’ aby przebudować pliki obiektowe z włączonym gcov:

[]$ make -B gcov
< compilation output >

for d in /Users/ykuperman/code/blogpost/qa/src/code ; do \
    FILES=`ls $d/*.c $d/*.cc $d/*.cpp 2> /dev/null` ; \
    gcov --object-directory objs/$d $FILES >> gcov_output.txt 2>>gcov_error.txt ; \
done

for f in  ; do \
    gcov --object-directory objs/$f $f >> gcov_output.txt 2>>gcov_error.txt ; \
done

/usr/local/scripts/filterGcov.sh gcov_output.txt gcov_error.txt gcov_report.txt example.txt

cat gcov_report.txt
100.00% /Users/ykuperman/code/blogpost/qa/src/code/code.cpp

mkdir -p gcov
mv *.gcov gcov
mv gcov_* gcov

See gcov directory for details

To wygeneruje szereg plików w nowym katalogu ‘gcov’. Są to:

  1. code.cpp.gcov – faktyczny plik ‘gcov’ dla testowanego kodu

  2. gcov_error.txt – raport błędów (w naszym przypadku powinien być pusty)

  3. gcov_output.txt – rzeczywisty wynik polecenia gcov, które zostało uruchomione

  4. gcov_report.txt – podsumowanie pokrycia dla każdego pliku, który był testowany

  5. gcov_report.txt.html – html-owa wersja raportu gcov

Cpputest Wykrywanie Wycieku Pamięci

Cpputest pozwala automatycznie wykrywać wycieki pamięci przez redefiniowanie standardowej rodziny funkcji „malloc/free”, aby zamiast tego używać własnych wrapperów. Dzięki temu może szybko wychwytywać wycieki i raportować je dla każdego wykonania testu. Jest to domyślnie włączone w MakefileWorker.mk, więc jest już aktywne z krokami opisanymi do tej pory.

By zilustrować, wycieknijmy trochę pamięci w test_func() !

Wracając do code.c, dodajemy malloc()  do funkcji, jak poniżej:

int test_func() {
    malloc(1);
    return 1;
}

Teraz, po ponownym skompilowaniu, pojawia się następujący błąd:

test.cpp:9: error: Failure in TEST(AwesomeExamples, FirstExample)
Memory leak(s) found.
Alloc num (4)
Leak size: 1
Allocated at: ./code.c and line: 6
Type: "malloc"
Memory: <

To pokazuje, który test spowodował wyciek, gdzie wyciek nastąpił w kodzie źródłowym i co znajdowało się w wyciekającej pamięci. Bardzo pomocne!

Istnieje kilka zastrzeżeń dotyczących tej funkcji:

  1. Cpputest używa makr preprocesora do dynamicznego redefiniowania wszystkich wywołań standardowych funkcji zarządzania pamięcią. To oznacza, że będzie działać tylko dla wywołań w kodzie źródłowym pod testem, ponieważ to właśnie ten kod jest kompilowany z nadpisaniami CppUTest. Wycieki w bibliotekach zewnętrznych nie będą wychwytywane.

  2. Czasami pamięć, która jest przydzielana na cały czas życia procesu, nie jest przeznaczona do zwolnienia. Może to powodować wiele spamowych błędów, jeśli testujesz moduł z takim zachowaniem. Aby wyłączyć wykrywanie wycieków, możesz to zrobić:

CPPUTEST_USE_MEM_LEAK_DETECTION=N

Zainteresowany czymś więcej?

To tylko wierzchołek góry lodowej, jeśli chodzi o wszystkie funkcje zawarte w tym narzędziu. Oprócz podstaw omówionych tutaj, posiada ono również framework do mockowania, bezpośrednią warstwę integracji z C oraz framework pluginów, żeby wymienić kilka znaczących. Repozytorium zawiera także cały katalog skryptów pomocniczych, które mogą pomóc zautomatyzować niektóre rutynowe czynności związane z pracą z frameworkiem.

Mam nadzieję, że informacje tutaj pomogą Ci poprawić jakość kodu C/C++ przy użyciu tego świetnego narzędzia!

Inne wiadomości

Czytaj więcej z tej kategorii

A person is standing at a desk while typing on a laptop.

Kompletna, natywna dla AI platforma, która rośnie wraz z Twoim biznesem.

A person is standing at a desk while typing on a laptop.

Kompletna, natywna dla AI platforma, która rośnie wraz z Twoim biznesem.