Startseite: https://github.com/mity/acutest
Acutest ist eine C/C++-Unit-Test-Funktion mit dem Ziel, so einfach wie möglich zu sein, dem Entwickler nicht im Weg zu stehen und externe Abhängigkeiten zu minimieren.
Um dies zu erreichen, befindet sich die vollständige Implementierung in einer einzigen C-Header-Datei und ihr Kern hängt nur von wenigen Standard-C-Bibliotheksfunktionen ab.
Hauptmerkmale:
acutest.h
.main()
).--tap
).--xml-output=FILE
).C++-spezifische Funktionen:
std::exception
abgeleitet ist, wird what()
in der Fehlermeldung ausgeschrieben.Unix/Posix-spezifische Funktionen:
clock_gettime()
) anbietet, kann der Benutzer die Testausführungszeiten mit --time=real
(wie --time
) und --time=cpu
messen.Linux-spezifische Funktionen:
Windows-spezifische Funktionen:
--time
messen.macOS-spezifische Funktionen:
Jedes C/C++-Modul, das einen oder mehrere Komponententests implementiert und acutest.h
enthält, kann als eigenständiges Programm erstellt werden. Für die Zwecke dieses Dokuments bezeichnen wir die resultierende Binärdatei als „Testsuite“. Anschließend wird die Suite ausgeführt, um die Tests auszuführen, wie in den Befehlszeilenoptionen angegeben.
Wir sagen, dass jeder Unit-Test genau dann erfolgreich ist, wenn:
Um Acutest zu verwenden, fügen Sie einfach die Header-Datei acutest.h
am Anfang der C/C++-Quelldatei ein, um einen oder mehrere Komponententests zu implementieren. Beachten Sie, dass der Header die Implementierung der Funktion main()
bereitstellt.
#include "acutest.h"
Jeder Test soll als Funktion mit folgendem Prototyp implementiert werden:
void test_example ( void );
Die Tests können einige Präprozessormakros verwenden, um die Testbedingungen zu validieren. Sie können mehrfach verwendet werden, und wenn eine dieser Bedingungen fehlschlägt, gilt der jeweilige Test als fehlgeschlagen.
TEST_CHECK
ist das am häufigsten verwendete Testmakro, das lediglich eine boolesche Bedingung testet und fehlschlägt, wenn die Bedingung als falsch (oder Null) ausgewertet wird.
Zum Beispiel:
void test_example ( void )
{
void * mem ;
int a , b ;
mem = malloc ( 10 );
TEST_CHECK ( mem != NULL );
mem = realloc ( mem , 20 );
TEST_CHECK ( mem != NULL );
}
Beachten Sie, dass die Tests völlig unabhängig voneinander sein sollten. Immer wenn die Testsuite aufgerufen wird, kann der Benutzer eine beliebige Anzahl von Tests in der Suite in beliebiger Reihenfolge ausführen. Darüber hinaus wird jeder Komponententest auf Plattformen, sofern unterstützt, standardmäßig als eigenständiger (Unter-)Prozess ausgeführt.
Schließlich muss die Quelldatei der Testsuite die Komponententests mithilfe des Makros TEST_LIST
auflisten. Die Liste gibt den Namen jedes Tests (er muss eindeutig sein) und einen Zeiger auf eine Funktion an, die den Test implementiert. Ich empfehle Namen, die einfach in der Befehlszeile verwendet werden können: Vermeiden Sie insbesondere Leerzeichen und andere Sonderzeichen, die in der Shell möglicherweise mit Escapezeichen versehen werden müssen. Vermeiden Sie außerdem den Bindestrich ( -
) als erstes Zeichen des Namens, da dieser dann als Befehlszeilenoption und nicht als Testname interpretiert werden könnte.
TEST_LIST = {
{ "example" , test_example },
...
{ NULL, NULL } /* zeroed record marking the end of the list */
};
Beachten Sie, dass die Testliste mit einem auf Null gesetzten Datensatz beendet werden muss.
Für eine einfache Testsuite ist das mehr oder weniger alles, was Sie wissen müssen. Acutest bietet jedoch einige weitere Makros, die in bestimmten Situationen nützlich sein können. Wir behandeln sie in den folgenden Unterabschnitten.
Es gibt ein Makro TEST_ASSERT
, das TEST_CHECK
sehr ähnlich ist, aber wenn es fehlschlägt, bricht es die Ausführung des aktuellen Komponententests sofort ab.
Zum Beispiel:
void test_example ( void )
{
void * mem ;
int a , b ;
mem = malloc ( 10 );
TEST_ASSERT ( mem != NULL );
mem = realloc ( mem , 20 );
TEST_ASSERT ( mem != NULL );
}
Der Abbruch im Fehlerfall erfolgt entweder durch den Aufruf von abort()
(wenn der Test als Kindprozess ausgeführt wird) oder über longjmp()
(falls nicht).
Daher sollte es nur verwendet werden, wenn Sie die Kosten verstehen, die mit einem solch brutalen Abbruch des Tests verbunden sind. Je nachdem, was Ihr Unit-Test tut, kann er ungelöschte Dateideskriptoren, Speicherlecks, zerstörte C++-Objekte ohne Aufruf ihrer Destruktoren und mehr umfassen.
Im Allgemeinen sollte TEST_CHECK
gegenüber TEST_ASSERT
bevorzugt werden, es sei denn, Sie wissen genau, was Sie tun und warum Sie TEST_ASSERT
in einer bestimmten Situation gewählt haben.
Für C++ gibt es ein zusätzliches Makro TEST_EXCEPTION
zum Überprüfen, ob der angegebene Code (normalerweise nur eine Funktion oder ein Methodenaufruf) den erwarteten Ausnahmetyp auslöst.
Die Prüfung schlägt fehl, wenn die Funktion keine Ausnahme auslöst oder wenn sie etwas Inkompatibles auslöst.
Zum Beispiel:
void test_example ( void )
{
TEST_EXCEPTION ( CallSomeFunction (), std:: exception );
}
Wenn eine Bedingungsprüfung fehlschlägt, ist es oft sinnvoll, zusätzliche Informationen über die Situation bereitzustellen, damit das Problem leichter behoben werden kann. Acutest stellt hierfür die Makros TEST_MSG
und TEST_DUMP
zur Verfügung.
Ersteres gibt eine beliebige printf
-ähnliche Nachricht aus, das andere gibt einen hexadezimalen Dump eines bereitgestellten Speicherblocks aus.
Also zum Beispiel:
void test_example ( void )
{
char produced [ 100 ];
char expected [] = "Hello World!" ;
SomeSprintfLikeFunction ( produced , "Hello %s!" , "world" );
TEST_CHECK ( strcmp ( produced , expected ) == 0 );
TEST_MSG ( "Expected: %s" , expected );
TEST_MSG ( "Produced: %s" , produced );
/* Or, if the function could provide some binary stuff, we might rather
* use TEST_DUMP instead in order to output a hexadecimal dump of the data.
*/
TEST_DUMP ( "Expected:" , expected , strlen ( expected ));
TEST_DUMP ( "Produced:" , produced , strlen ( produced ));
}
Beachten Sie, dass beide Makros nur dann etwas ausgeben, wenn das letzte Überprüfungsmakro fehlgeschlagen ist. Mit anderen Worten, diese beiden sind gleichwertig:
if (! TEST_CHECK ( some_condition != 0 ))
TEST_MSG ( "some message" );
TEST_CHECK ( some_condition != 0 );
TEST_MSG ( "some message" );
(Beachten Sie, dass TEST_MSG
den Compiler mit Unterstützung für verschiedene Makros erfordert.)
Manchmal ist es sinnvoll, Ihre Testfunktion als Schleife über Daten zu entwerfen, die eine Sammlung von Testvektoren und ihren jeweiligen erwarteten Ausgaben bereitstellt. Stellen Sie sich zum Beispiel vor, dass unser Komponententest eine Art Hashing-Funktionsimplementierung überprüfen soll und wir in der Hash-Spezifikation eine Sammlung von Testvektoren dafür haben.
In solchen Fällen ist es sehr nützlich, jedem Testvektor einen Namen zuzuordnen und den Namen im Ausgabeprotokoll auszugeben, damit bei fehlgeschlagener Prüfung der fehlerhafte Testvektor leicht identifiziert werden kann. Der Schleifenkörper kann jedoch Dutzende von Prüfmakros ausführen, sodass es unpraktisch sein kann, einen solchen Namen hinzuzufügen, um jede Prüfmeldung in der Schleife anzupassen.
Um dieses Problem zu lösen, stellt Acutest das Makro TEST_CASE
zur Verfügung. Das Makro gibt eine Zeichenfolge an, die als Name des Testvektors dient. Bei der Verwendung stellt Acutest sicher, dass im Ausgabeprotokoll der angegebene Name vor jeder Meldung aus nachfolgenden Bedingungsprüfungen steht.
Nehmen wir zum Beispiel an, wir testen SomeFunction()
das für ein bestimmtes Byte-Array einer bestimmten Größe ein weiteres Byte-Array in einem neu malloc
-ed Puffer zurückgeben soll. Dann können wir so etwas machen:
struct TestVector {
const char * name ;
const uint8_t * input ;
size_t input_size ;
const uint8_t * expected_output ;
size_t expected_output_size ;
};
struct TestVector test_vectors [] = {
/* some data */
};
void test_example ( void )
{
int i ;
const uint8_t * output ;
size_t output_size ;
for ( i = 0 ; i < sizeof ( test_vectors ) / sizeof ( test_vectors [ 0 ]); i ++ ) {
struct TestVector * vec = & test_vectors [ i ];
/* Output the name of the tested test vector. */
TEST_CASE ( vec . name );
/* Now, we can check the function produces what it should for the
* current test vector. If any of the following checking macros
* produces any output (either because the check fails, or because
* high `--verbose` level is used), Acutest also outputs the currently
* tested vector's name. */
output = SomeFunction ( vec -> input , vec -> input_size , & output_size );
if ( TEST_CHECK ( output != NULL )) {
TEST_CHECK ( output_size == vec -> expected_output_size );
TEST_CHECK ( memcmp ( output , vec -> expected_output , output_size ) == 0 );
free ( output );
}
}
}
Der angegebene Name gilt für alle Prüfungen, die nach der Verwendung von TEST_CASE
ausgeführt werden
TEST_CASE
erneut verwendet wird, um einen anderen Namen anzugeben; oderTEST_CASE
mit NULL
als Argument verwendet wird.Viele der in den vorherigen Abschnitten erwähnten Makros verfügen über ein Gegenstück, das die Ausgabe benutzerdefinierter Nachrichten anstelle einiger Standardnachrichten ermöglicht.
Alle diese haben den gleichen Namen wie die oben genannten Makros, nur mit dem Zusatz Unterstrich. Mit dem Suffix erwarten sie dann printf
-ähnliches String-Format und entsprechende zusätzliche Argumente.
Also zum Beispiel statt der einfachen Prüfmakros
TEST_CHECK (a == b);
TEST_ASSERT (x < y);
TEST_EXCEPTION (SomeFunction(), std::exception);
Wir können ihre jeweiligen Gegenstücke mit benutzerdefinierten Nachrichten verwenden:
TEST_CHECK_ (a == b, " %d is equal to %d " , a, b);
TEST_ASSERT_ (x < y, " %d is lower than %d " , x, y);
TEST_EXCEPTION_ (SomeFunction(), std::exception, "SomeFunction() throws std::exception");
Sie sollten dafür eine neutrale Formulierung verwenden, da mit der Befehlszeilenoption --verbose
die Nachrichten auch dann abgemeldet werden, wenn die entsprechende Prüfung erfolgreich verläuft.
(Wenn Sie für den Fall, dass die Prüfung fehlschlägt, Diagnoseinformationen ausgeben müssen, verwenden Sie das Makro TEST_MSG
. Genau das ist sein Zweck.)
Ebenso statt
TEST_CASE ( "name" );
wir können reicher verwenden
TEST_CASE_ ( "iteration #%d" , 42 );
Beachten Sie jedoch, dass all dies nur verwendet werden kann, wenn Ihr Compiler verschiedene Präprozessormakros unterstützt. Variadische Makros wurden mit C99 zum Standardbestandteil der C-Sprache.
Wenn wir mit der Implementierung der Tests fertig sind, können wir sie einfach als einfaches C/C++-Programm kompilieren. Angenommen, cc
ist Ihr C-Compiler:
$ cc test_example.c -o test_example
Wenn die Testsuite kompiliert wird, kann die resultierende Testbinärdatei zum Ausführen der Tests verwendet werden.
Der Exit-Code der Testsuite ist 0, wenn alle ausgeführten Komponententests erfolgreich sind, 1, wenn einer davon fehlschlägt, oder eine beliebige andere Zahl, wenn ein interner Fehler auftritt.
Standardmäßig (ohne Befehlszeilenoptionen) werden alle implementierten Komponententests ausgeführt. Es kann auch nur eine Teilmenge der in der Befehlszeile angegebenen Komponententests ausführen:
$ ./test_example # Runs all tests in the suite
$ ./test_example test1 test2 # Runs only tests specified
$ ./test_example --exclude test3 # Runs all tests but those specified
Beachten Sie, dass ein einzelnes Befehlszeilenargument eine ganze Gruppe von Testeinheiten auswählen kann, da Acutest mehrere Ebenen der Komponententestauswahl implementiert (die erste, die mindestens einer Testeinheit entspricht, wird verwendet):
Genaue Übereinstimmung : Wenn das Argument genau mit dem gesamten Namen eines Komponententests übereinstimmt, wird nur der angegebene Test ausgewählt.
Wortübereinstimmung : Wenn das Argument mit keinem vollständigen Testnamen, aber mit einem ganzen Wort in einem oder mehreren Testnamen übereinstimmt, werden alle diese Tests ausgewählt.
Die folgenden Zeichen werden als Worttrennzeichen erkannt: Leerzeichen
, Tabulator t
, -
, Unterstrich _
, Schrägstrich /
, Punkt .
, Komma ,
, Doppelpunkt :
, Semikolon ;
.
Teilzeichenfolgenübereinstimmung : Wenn selbst die Wortübereinstimmung keinen Test auswählen konnte, werden alle Tests ausgewählt, deren Name das Argument als Teilzeichenfolge enthält.
Durch die Übernahme einer geeigneten Testbenennungsstrategie können Benutzer problemlos eine ganze Familie verwandter Tests mit einem einzigen Befehlszeilenargument ausführen (oder überspringen, wenn --exclude
verwendet wird).
Betrachten Sie zum Beispiel die Testsuite test_example
die die Tests foo-1
, foo-2
, foomatic
, bar-1
und bar-10
implementiert:
$ ./test_example bar-1 # Runs only the test 'bar-1' (exact match)
$ ./test_example foo # Runs 'foo-1' and 'foo-2' (word match)
$ ./test_example oo # Runs 'foo-1', 'foo-2' and 'foomatic' (substring match)
$ ./test_example 1 # Runs 'foo-1' and 'bar-1' (word match)
Sie können --list
oder -l
verwenden, um einfach alle von der angegebenen Testsuite implementierten Unit-Tests aufzulisten:
$ ./test_example --list
Um eine Beschreibung aller unterstützten Befehlszeilenoptionen anzuzeigen, führen Sie die Binärdatei mit der Option --help
aus:
$ ./test_example --help
F: War dieses Projekt nicht als „CUTest“ bekannt?
A: Ja. Es wurde umbenannt, da sich herausstellte, dass der ursprüngliche Name zu überladen war.
F: Muss ich die Dateien README.md
und/oder LICENSE.md
verteilen?
A: Nein. Der Header acutest.h
enthält die URL zu unserem Repo, den Copyright-Hinweis und die MIT-Lizenzbedingungen. Solange Sie diese beibehalten, ist es völlig in Ordnung, wenn Sie Ihrem Projekt nur den Header hinzufügen. Schließlich ist die einfache Bedienung und der All-in-One-Header-Charakter unser oberstes Ziel.
Acutest unterliegt einer MIT-Lizenz. Den vollständigen Text finden Sie in der Datei LICENSE.md
oder am Anfang von acutest.h
.
Das Projekt befindet sich auf Github:
Dort können Sie die neueste Version von Acutest finden, mit Verbesserungen beitragen oder Fehler melden.