Web
Analytics
 

A Minimal Guide to CUnit

Table of Contents

As long as this note remains on this page, any information is subject to change and broken links, etc. are to be expected. Please be restrictive in reporting any errors for now.

1 Inledning

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:

  1. Sätta upp testet: skapa kataloger och filer för loggmeddelanden
  2. 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
  3. Utför testen \(\{a,b,c,d\}\times\{e,f,g\}\)1 och jämför det förväntade utdatat med det faktiska utfallet och signalera fel
  4. Riva ned testet: ta bort skapade kataloger och filer

1.1 Grundläggande CUnit

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();

1.2 Att sätta upp och riva ned tester

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).

1.3 Utföra tester

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.

1.4 Arbeta på egen maskin

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/.

2 Kompilera med CUnit

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.

3 En minimal CUnit-fil

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();
}

4 Filen unittests.c

#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();
}

Questions about stuff on these pages? Use our Piazza forum.

Want to report a bug? Please place an issue here. Pull requests are graciously accepted (hint, hint).

Nerd fact: These pages are generated using org-mode in Emacs, a modified ReadTheOrg template, and a bunch of scripts.

Ended up here randomly? These are the pages for a one-semester course at 67% speed on imperative and object-oriented programming at the department of Information Technology at Uppsala University, ran by Tobias Wrigstad.

Footnotes:

1
Där t.ex. \((a,g)\) avser ett test med en tom fil till vilken ett långt meddelande skrivs.

Author: Tobias Wrigstad

Created: 2018-10-12 fre 20:04

Validate