CUnit är ett ramverk för att skriva och utföra enhetstester på C-kod. CUnit är utvecklat som ett biliotek av funktioner som länkas samman med användarens testkod.
Denna enkla lathund är menad att komplettera CUnit Programmers
Guide. Den visar hur man snabbt kan komma igång med CUnit, samt
den kompilatorflagga som måste anges vid kompilering med CUnit på
institutionens datorsystem. Om tillämpligt, titta på några av
enhetstesterna som distributerats med koden för
inlämningsuppgifterna (här tar vi exempel från en fil
unittests.c
från en inlämningsuppgift från en forntida IOOPM,
see här) och försök följa dem för att se hur enkelt enhetstest kan
sättas upp.
Ett enhetstest är ett test av en enhet, t.ex. en modul eller sammanhängande samling av funktioner. (Täcks av föreläsning på kursen.) Ett enhetstest av en modul för att logga programmeddelanden på disk kunde t.ex. bestå av följande komponenter:
- Sätta upp testet: skapa kataloger och filer för loggmeddelanden
- Test att logga på:
$a$ en tom fil,$b$ en fil med en rad text i,$c$ en fil med många rader text i,$d$ en fil som inte finns – med –$e$ ett tomt loggmeddelande,$f$ ett meddelande med ett tecken,$g$ ett långt meddelande - Utför testen
$\{a,b,c,d\}×\{e,f,g\}$ [fn::Där t.ex.$(a,g)$ avser ett test med en tom fil till vilken ett långt meddelande skrivs.] och jämför det förväntade utdatat med det faktiska utfallet och signalera fel - Riva ned testet: ta bort skapade kataloger och filer
Ett enhetstest i CUnit består av ett antal testsviter som var och
en innehåller ett antal olika test. Filen unittests.c
(här) i
som vi använder som löpande exempel har tre sviter, en för test av
ett binärt sökträd, ett för en enkellänkade lista och ett för att
läsa ord från en inström.
Följande kod skapar den sistnämnda sviten i variabeln
pSuiteNW
Först deklareras variabeln (rad 1), sedan skapas
sviten (rad 6) med ett namn ''nextWord Suite''
samt
funktionerna för att sätta upp samt riva ned testen
(init_suite_nw
och clean_suite_nw
, se nästa
avsnitt. Om sviten inte skapades korrekt är värdet i pSuiteNW
NULL
vi städar bland de registrerade testerna (rad 9) och
avslutar genom att returnera en felkod (rad 10).
Rad 13–16 lägger till ett test i sviten. Funktionen
CU_add_test
lägger till testfunktionen test_next_word
en
funktion som vi (utvecklaren) skrivit själva, i sviten med ett
beskrivande namn. Fel fångas upp och rapporteras på samma sätt som
tidigare.
På rad 18 anges att vi vill att testen skall utföras verbose, alltså att alla detaljer skall skrivas ut när testen körs. Rad 19 kör alla test (i detta fall bara ett). Rad 20 städar upp bland de registrerade testen (avallokerar minne, etc.). Slutligen, på rad 21, returnerar vi de eventuella fel som uppstått under körning.
CU_pSuite pSuiteNW = NULL;
if (CUE_SUCCESS != CU_initialize_registry())
return CU_get_error();
pSuiteNW = CU_add_suite("nextWord Suite", init_suite_nw, clean_suite_nw);
if (NULL == pSuiteNW) {
CU_cleanup_registry();
return CU_get_error();
}
if (NULL == CU_add_test(pSuiteNW, "test of nextWord()", test_next_word)) {
CU_cleanup_registry();
return CU_get_error();
}
CU_basic_set_mode(CU_BRM_VERBOSE);
CU_basic_run_tests();
CU_cleanup_registry();
return CU_get_error();
För vissa samlingar av test kan det vara smidigt att först utföra ett initialt arbete. Det kan röra sig om att skapa filer och kataloger i filsystemet där testerna kommer att skriva och läsa, eller t.ex. skapa ett antal binära sökträd som testerna sedan opererar på.
I unittests.c
finns följande två funktioner.
int init_suite_bst(void) {
return 0;
}
int init_suite_nw(void) {
temp_file = fopen("temp.txt", "w+"); // global variabel
if (temp_file == NULL {
return -1;
} else {
return 0;
}
}
Den första funktionen initierar alla test av modulen bst
–
det binära sökträdet, och gör som synes ingenting. Alla tester av
sökträdet skapar ett nytt träd och utför testerna på det.
Den andra funktionen initierar alla test av modulen för
nextWord
och öppnar filen temp.txt
för skrivning i den
aktuella katalogen. Om detta inte är möjligt kommer ett fel
signaleras och testen inte utföras vidare – vilket är rimligt då
förutsättningarna för att läsa in ord uppenbarligen inte finns.
En motsvarande funktion river också ned nextWord
sviten av
tester:
int clean_suite_nw(void) {
if (0 != fclose(temp_file)) {
return -1;
} else {
temp_file = NULL;
return 0;
}
}
Här stängs filen i fråga, varvid vi kan rapportera att nedrivning
av testen gick enligt planen (return 0
).
Varje funktion vars namn börjar på test
avser ett test av en
funktion i enheten. Följande test som fanns bland
enhetstesterna till en gammal inlämningsuppgift 1 (fram till 2017) testar att insertering i
ett binärt sökträd skapar ett träd med förväntat djup.
Funktionen int depth(tree_t *)
används för att ta reda på
trädets djup. Denna funktion är inte strikt nödvändig för
insertering och sökning, men är en hjälpfunktion som utvecklaren
av trädet tillhandahåller bl.a. just för att underlätta test av
trädet. Det är relativt vanligt att tillhandahålla ”extra kod”
på detta sätt. Det underlättar testandet och håller dessutom
testen på en rimlig abstraktionsnivå! I föreliggande exempel,
om trädets interna representation ändras behöver vi inte skriva om
testet. Mycket smidigt. Men ack! Den sista raden i testet nedan
följer inte denna princip.
I koden nedan skapas ett nytt träd. För varje insertering
kontrolleras att det resulterande trädets djup är den förväntade.
Detta görs med CU_ASSERT(<exp>)
där <exp>
är ett booleskt
uttryck som förväntas evaluera till sant. Om något uttryck i
någon assert-sats evaluerar till falskt (vi säger att asserten
fejlar, alt. misslyckas, på Svenska) under testet räknas testet
som att det inte har passerat.
void test_bst_depth(void) {
tree_t *t = insert(NULL, "ni", 1);
CU_ASSERT(depth(t) == 1);
t = insert(t, "spam", 2);
CU_ASSERT(depth(t) == 2);
t = insert(t, "eki", 3);
CU_ASSERT(depth(t) == 2);
t = insert(t, "eki", 4);
CU_ASSERT(depth(t) == 2);
CU_ASSERT(strcmp(t->left->key, "eki") == 0); // Bryter mot abstraktionsprincipen!
/// OBS! Här borde vi städa bort data från heapen som allokerats av testet.
Observera att testet inte returnerar något. Om funktionen körs utan att någon assert-sats misslyckas anses testet ha passerat.
De olika typerna av assertions som finns är dokumenterade här.
Om man ville undvika att bryta mot abstraktionsprincipen skulle man kunna utveckla en funktion som tillät åtkomst till en specifik nod i trädet som en del av trädmodulen. Man skulle t.ex. kunna skriva följande:
char *key_for_path(tree_t *t, char *path) {
if (!t) return NULL;
switch (*path) {
case 'L': return key_for_path(t->left, ++path);
case 'R': return key_for_path(t->right, ++path);
case '\0': return t->key;
default:
printf(stderr, "Bogus path value '%c' expected L or R\n", *path);
}
return NULL;
}
Denna funktion vandrar i trädet i enlighet med en söksträng.
T.ex. returnerar ''LRLLR''
den nyckel som fås efter att först gå
vänster, sedan höger, sedan två gånger vänster, och sist höger i
trädet.
Nu skulle man kunna skriva om sista raden i testet så här:
char *temp = key_for_path(t, "L");
CU_ASSERT(strcmp(temp, "eki") == 0);
Ofta kan det vara en bra idé att inte lägga denna typ av funktion i sin moduls headerfil, utan istället skapa en speciell headerfil som är specifik för testning och som definierar de ytterligare funktionerna.
Titta på de olika test
funktionerna i unittests.c
i
inlämningsuppgift 1 för att se olika exempel på tester av både ett
binärt sökträd och en enkellänkad lista.
Om du vill arbeta på din egen maskin måste du installera CUnit. Detta handleds endast i mån av tid och förmåga, givet att din dator är åtkomlig från någon av institutionens datorsalar.
CUnit finns att ladda ned på http://cunit.sourceforge.net/.
Vid kompilering med CUnit måste man explicit ange att CUnit skall
länkas in. Detta görs med flaggan -l
med parametern cunit
, t.ex.:
gcc -ggdb -Wall -std=c11 unittests.c list.c bst.c -o unittests -lcunit
Om -lcunit
inte anges kommer länknings-steget efter
kompileringen att misslyckas, eftersom funktionerna för
enhetstest, t.ex. CUAssert
då fortfarande saknas.
Använd nedanstående som mall för att komma igång med CUnit.
#include <string.h>
#include <stdbool.h>
#include <CUnit/Basic.h>
int init_suite(void)
{
return 0;
}
int clean_suite(void)
{
return 0;
}
void test1(void)
{
CU_ASSERT(true);
}
void test2(void)
{
CU_ASSERT(true);
}
int main()
{
CU_pSuite test_suite1 = NULL;
if (CUE_SUCCESS != CU_initialize_registry())
return CU_get_error();
test_suite1 = CU_add_suite("Test Suite 1", init_suite, clean_suite);
if (NULL == test_suite1)
{
CU_cleanup_registry();
return CU_get_error();
}
if (
(NULL == CU_add_test(test_suite1, "test 1", test1)) ||
(NULL == CU_add_test(test_suite1, "test 2", test2))
)
{
CU_cleanup_registry();
return CU_get_error();
}
CU_basic_set_mode(CU_BRM_VERBOSE);
CU_basic_run_tests();
CU_cleanup_registry();
return CU_get_error();
}
#include <string.h>
#include "CUnit/Basic.h"
#include "list.h"
#include "bst.h"
#include "nextword.h"
static FILE* temp_file = NULL;
int init_suite_bst(void)
{
return 0;
}
int clean_suite_bst(void)
{
return 0;
}
int init_suite_list(void)
{
return 0;
}
int clean_suite_list(void)
{
return 0;
}
int init_suite_nw(void)
{
if (NULL == (temp_file = fopen("temp.txt", "w+")))
{
return -1;
}
else
{
return 0;
}
}
int clean_suite_nw(void)
{
if (0 != fclose(temp_file))
{
return -1;
}
else
{
temp_file = NULL;
return 0;
}
}
void test_bst_insert(void)
{
tree_t *t = tree_insert(NULL, "spam\0", 1);
CU_ASSERT(strcmp(t->key, "spam\0") == 0);
CU_ASSERT(t != NULL);
CU_ASSERT(t->left == NULL);
CU_ASSERT(t->right == NULL);
CU_ASSERT(t->rowlist != NULL);
CU_ASSERT(listlength(t->rowlist) == 1);
t = tree_insert(t, "spam\0", 2);
CU_ASSERT(listlength(t->rowlist) == 2);
tree_t *d = tree_insert(NULL, "ni\0", 1);
d = tree_insert(d, "spam\0", 2);
d = tree_insert(d, "eki\0", 3);
CU_ASSERT(strcmp(d->key, "ni\0") == 0);
CU_ASSERT(strcmp(d->right->key, "spam\0") == 0);
CU_ASSERT(strcmp(d->left->key, "eki\0") == 0);
/// OBS! Här borde vi städa bort data från heapen som allokerats av testet.
}
void test_bst_remove(void)
{
/* Note that this test is not run automatically, modifications
further down are necessary */
}
void test_bst_depth(void)
{
tree_t *t = tree_insert(NULL, "ni\0", 1);
CU_ASSERT(depth(t) == 1);
t = tree_insert(t, "spam\0", 2);
CU_ASSERT(depth(t) == 2);
t = tree_insert(t, "eki\0", 3);
CU_ASSERT(depth(t) == 2);
t = tree_insert(t, "eki\0", 4);
CU_ASSERT(depth(t) == 2);
CU_ASSERT(strcmp(t->left->key, "eki\0") == 0);
/// OBS! Här borde vi städa bort data från heapen som allokerats av testet.
}
void test_bst_size(void)
{
tree_t *t = tree_insert(NULL, "ni\0", 1);
CU_ASSERT(size(t) == 1);
t = tree_insert(t, "spam\0", 2);
CU_ASSERT(size(t) == 2);
t = tree_insert(t, "eki\0", 3);
CU_ASSERT(size(t) == 3);
t = tree_insert(t, "eki\0", 4);
CU_ASSERT(size(t) == 3);
/// OBS! Här borde vi städa bort data från heapen som allokerats av testet.
}
void test_list_insert(void)
{
link_t *l = listinsert(1, NULL);
for (int i=2; i<=10; i++)
{
l = listinsert(i, l);
}
for (int i=1; i<=10; i++)
{
CU_ASSERT(l->value == i);
l = l->next;
}
/// OBS! Här borde vi städa bort data från heapen som allokerats av testet.
}
void test_list_length(void)
{
link_t *l = listinsert(1, NULL);
CU_ASSERT(listlength(l) == 1);
for (int i=2; i<=20; i++)
{
l = listinsert(i, l);
CU_ASSERT(listlength(l) == i);
}
/// OBS! Här borde vi städa bort data från heapen som allokerats av testet.
}
void test_next_word(void)
{
char buffer[20];
CU_ASSERT(temp_file != NULL) // Internal error
fprintf(temp_file, "spam spam\nbacon spam");
rewind(temp_file);
// Läser in nästa ord i strömen "fp". Returnerar 0 vid EOF och 2 vid
// radbrytning, annars 1.
int i = nextWord(buffer, temp_file);
CU_ASSERT(strcmp(buffer, "spam\0") == 0)
CU_ASSERT(i == 1)
i = nextWord(buffer, temp_file);
CU_ASSERT(strcmp(buffer, "spam\0") == 0)
CU_ASSERT(i == 1)
i = nextWord(buffer, temp_file);
CU_ASSERT(i == 2)
i = nextWord(buffer, temp_file);
CU_ASSERT(strcmp(buffer, "bacon\0") == 0)
CU_ASSERT(i == 1)
i = nextWord(buffer, temp_file);
CU_ASSERT(strcmp(buffer, "spam\0") == 0)
CU_ASSERT(i == 1)
i = nextWord(buffer, temp_file);
CU_ASSERT(i == 0)
}
int main()
{
CU_pSuite pSuiteBst = NULL;
CU_pSuite pSuiteList = NULL;
CU_pSuite pSuiteNW = NULL;
if (CUE_SUCCESS != CU_initialize_registry())
return CU_get_error();
pSuiteNW = CU_add_suite("nextWord Suite", init_suite_nw, clean_suite_nw);
if (NULL == pSuiteNW)
{
CU_cleanup_registry();
return CU_get_error();
}
pSuiteList = CU_add_suite("Linked List Suite", init_suite_list, clean_suite_list);
if (NULL == pSuiteList)
{
CU_cleanup_registry();
return CU_get_error();
}
pSuiteBst = CU_add_suite("Binary Search Tree Suite", init_suite_bst, clean_suite_bst);
if (NULL == pSuiteBst)
{
CU_cleanup_registry();
return CU_get_error();
}
if (
(NULL == CU_add_test(pSuiteBst, "test of insert()", test_bst_insert)) ||
(NULL == CU_add_test(pSuiteBst, "test of size()", test_bst_size)) ||
(NULL == CU_add_test(pSuiteBst, "test of depth()", test_bst_depth))
)
{
CU_cleanup_registry();
return CU_get_error();
}
if (
(NULL == CU_add_test(pSuiteList, "test of listinsert()", test_list_insert)) ||
(NULL == CU_add_test(pSuiteList, "test of listlength()", test_list_length))
)
{
CU_cleanup_registry();
return CU_get_error();
}
if (
(NULL == CU_add_test(pSuiteNW, "test of nextWord()", test_next_word))
)
{
CU_cleanup_registry();
return CU_get_error();
}
CU_basic_set_mode(CU_BRM_VERBOSE);
CU_basic_run_tests();
CU_cleanup_registry();
return CU_get_error();
}