Ceedling Tutorial Swap()

Anforderungen

Zu Begin einer Softwareentwicklung unter TDD sollte stehts eine List der Anforderungen stehen. In diesem Fall verwenden wir eine einfache Auflistung der Funktionalität welche die Funktion swap() bieten soll.

  • Tauschen der Werte von zwei int.

  • Name swap().

  • Schnittstelle (int*, int*).

  • Rückgabewert int definiert für EXIT_SUCCESS und EXIT_FAILURE aus der C-Standard Library.

  • Exception handling von NULL Pointern.

Vorbereitung

: ~
$ mkdir -p myFirstUnitTests/{src,inc,doc,test_doc,test_src}

Erstelle einen neuen Ordner für die Unit-Tests und die dazugehörigen Unterordner.


: ~
$ cd myFirstUnitTests
: ~/myFirstUnitTests
$ ceedling new unitTests && \
mv unitTests/* test_src/ && \
rm -d unitTests

Erstelle ein neues ceedling Projekt mit dem Namen “unitTests” und verschiebe anschliessend den Inhalt des ordners nach “test_src”.


: ~/myFirstUnitTests
$ cd test_src/ && \
ceedling module:create[swap]

Erstelle ein neues Modul mit Hilfe des Befehls ceedling module:create[<MODULE_NAME>]. Dadurch werden im Ordner “src” ein .h und ein .c File mit dem entsprechenden Modulnamen erstellt. Ausserdem wird im Ordner “test” ein .c File erstellt, welches für die Unit-Tests verwendet werden kann.


Da wir die Implementation klar von den Unit-Tests trennen wollen, verschieben wir das eben kreierte Modul in die entsprechenden Ordner für die Implementation.

: ~/myFirstUnitTests/test_src
$ mv src/swap.c ../src/ && \
mv src/swap.h ../inc/

Damit ceedling das Modul weiterhin finden kann, muss das project.yml File angepasst werden. Dazu wird die Rubrik “source” um die entsprechenden Pfade erwertert.

:paths:
  :test:
    - +:test/**
    - -:test/support
  :source:
    - src/**
    - ../src/
    - ../inc/
  :support:
    - test/support

0. Funktions Dokumentation Schreiben

Um gegen eine Implementation testen zu können, muss zuerst einmal eine Implementation existieren. Dies ist notwendig da für die Tests gegen eine bestehende Implementation gelinkt werden muss. Da die eigentliche Implementation jedoch in einem späteren Schritt erfolgt, wird nur eine “leere” Implementation erstellt. Dies ist der richtige Zeitpunkt um bereits die Funktionalität zu dokumentieren.

swap.h

#ifndef _SWAP_H
#define _SWAP_H

/**
 * @brief Swapping two integer values.                                                                   
 *
 * The function takes two singly pointers to integers and swapps the referenced
 * values. On passed "NULL" pointers, the function returns imedately without
 * touching any referenced value. There are two retrun states defined which are 
 * part of the C-Standard library, "EXIT_SUCCESS" and "EXIT_FAILURE".
 *
 * @param key1 pointer to the first integer value.
 * @param key2 pointer to the second integer value.
 * @return EXIT_SUCCESS on successful execution, EXIT_FAILURE otherwise.
 */
int swap(int *key1, int *key2);

#endif // _SWAP_H

swap.c

#include <stdio.h>
#include <stdlib.h>

#include "swap.h"

int swap(int *key1, int *key2){}

Für das .c File wird ein zusätzlicher Header stdio.h inkludiert, denn die Funktion soll in der Lage sein auch Fehlermeldungen auszugeben. Für die definierten Rückgabewerte muss ein weiterer Header verwendet werden, stdlib.h.

0. Test Dokumentation Schreiben

Da die Anforderungen bekannt sind, kann mit der Erstellung der Tests begonnen werden. Es empfiehlt sich jedoch, vor gängig die Dokumentation der Tests zu schreiben. Somit ist man das erste Mal gezwungen die Notwendigkeit des Testes in Worte zu fassen. Dieser Schritt hilft bei der späteren Implementation der eigentlichen Tests.

Dazu wird das File test_swap.c bearbeitet.

test_swap.c

#include "unity.h"                                                                            
#include "swap.h"

void setUp(void)
{
}

void tearDown(void)
{
}

void test_swap_NeedToImplement(void)
{
    TEST_IGNORE_MESSAGE("Need to Implement swap");
}

Die Funktionen setUp() und tearDown werden vor bzw. nach jeder einzelnen Testfunktion aufgerufen. In diesem Beispiel werden sie nicht verwendet und können daher entfernt werden. Die Funktion test_swap_NeedToImplement ist eine Dummy-Funktion und kann auch entfernt werden. Für den Anfang wollen wir drei Tests implementieren:

  • test_swap_NULL_1 übergibt für den ersten Parameter der Funktion NULL.

  • test_swap_NULL_2 übergibt für den zweiten Parameter der Funktion NULL.

  • test_swap_behavior_1 überprüft den Tausch der beiden Werte welche der Funktion übergeben wurden.

test_swap.c

#include "unity.h"
#include "swap.h"

/**
 * @brief Test with argument "NULL" against parameter "int* key1".
 *
 * @test  Pass "NULL" as argument for parameter "int* key1".
 *        Pass a valid reference to an integer as argument for parameter
 *        "int* key2".
 *        Expect "EXIT_FAILURE" as return value.
 *        Expect no memory access violation.
 *
 * @note  The function must prevent the user against memory access violation
 *        due to de-referencing a "NULL" pointer.
 */
void test_swap_NULL_1(void)
{
}

/**
 * @brief Test with argument "NULL" against parameter "int* key2".
 *
 * @test  Pass "NULL" as argument for parameter "int* key2".
 *        Pass a valid reference to an integer as argument for parameter
 *        "int* key2".
 *        Expect "EXIT_FAILURE" as return value.
 *        Expect no memory access violation.
 *
 * @note  The function must prevent the user against memory access violation
 *        due to de-referencing a "NULL" pointer.
 */
void test_swap_NULL_2(void)
{
}

/**
 * @brief Test for correct swapping integer values.
 *
 * @test  Pass a valid reference to an integer as argument for parameter
 *        "int* key1".
 *        Pass a valid reference to an integer as argument for parameter
 *        "int* key2".
 *        Expect swapping of values referenced by passed arguments.                                      
 *        Expect "EXIT_SUCCESS" as return value.
 */
void test_swap_behavior_1(void)
{
}

1. Test Implementation Schreiben

Die eigentliche Implementation der Tests wird mit Hilfe der ASSERT Makros des unity Frameworks realisiert. Die vollständige List der zurverfügung stehenden Makros findes sich hier. Es bietet sich an, die folgende Reihenfolge der Argumente zu verfolgen: TEST_ASSERT_EQUAL(<Erwartungswert>,<IstWert>).

test_swap.c

#include "unity.h"
#include "swap.h"

/**
 * @brief Test with argument "NULL" against parameter "int* key1".
 *
 * @test  Pass "NULL" as argument for parameter "int* key1".
 *        Pass a valid reference to an integer as argument for parameter
 *        "int* key2".
 *        Expect "EXIT_FAILURE" as return value.
 *        Expect no memory access violation.
 *
 * @note  The function must prevent the user against memory access violation
 *        due to de-referencing a "NULL" pointer.
 */
void test_swap_NULL_1(void)
{
    printf("Test Function: [%s]\n", __FUNCTION__);

    puts("Setup ==");
    int key_2 = 43;

    puts("Start >>");
    TEST_ASSERT_EQUAL(EXIT_FAILURE, swap(NULL, &key_2));

    puts("Stop <<\n\n");
}

/**
 * @brief Test with argument "NULL" against parameter "int* key2".
 *
 * @test  Pass "NULL" as argument for parameter "int* key2".
 *        Pass a valid reference to an integer as argument for parameter
 *        "int* key2".
 *        Expect "EXIT_FAILURE" as return value.
 *        Expect no memory access violation.
 *
 * @note  The function must prevent the user against memory access violation
 *        due to de-referencing a "NULL" pointer.
 */
void test_swap_NULL_2(void)
{
    printf("Test Function: [%s]\n", __FUNCTION__);

    puts("Setup ==");
    int key_1 = 42;

    puts("Start >>");
    TEST_ASSERT_EQUAL(EXIT_FAILURE, swap(&key_1, NULL));

    puts("Stop <<\n\n");
}

/**
 * @brief Test for correct swapping integer values.
 *
 * @test  Pass a valid reference to an integer as argument for parameter
 *        "int* key1".
 *        Pass a valid reference to an integer as argument for parameter
 *        "int* key2".
 *        Expect swapping of values referenced by passed arguments. 
 *        Expect "EXIT_SUCCESS" as return value.
 */
void test_swap_behavior_1(void)
{
    printf("Test Function: [%s]\n", __FUNCTION__);

    puts("Setup ==");
    int key_1 = 42;
    int key_2 = 43;

    puts("Start >>");
    TEST_ASSERT_EQUAL(EXIT_SUCCESS, swap(&key_1, NULL));
    
    // key_1
    TEST_ASSERT_EQUAL(43, key_1);

    // key_2
    TEST_ASSERT_EQUAL(42, key_2);

    puts("Stop <<\n\n");
}

2. Test Prüfen

Nun kann ceedling mit dem Befehl ceedling ausgeführt werden. Zu diesem Zeitpunkt sollten alle Tests scheitern. Eine mögliche Ausgabe könnte folgendermassen aussehen:

$ ceedling


Test 'test_swap.c'
------------------
Generating runner for test_swap.c...
Compiling test_swap_runner.c...
Compiling test_swap.c...
Linking test_swap.out...
Running test_swap.out...

-----------
TEST OUTPUT
-----------
[test_swap.c]
  - "Test Function: [test_swap_NULL_1]"
  - "Setup =="
  - "Start >>"
  - "Test Function: [test_swap_NULL_2]"
  - "Setup =="
  - "Start >>"
  - "Test Function: [test_swap_behavior_1]"
  - "Setup =="
  - "Start >>"

-------------------
FAILED TEST SUMMARY
-------------------
[test_swap.c]
  Test: test_swap_NULL_1
  At line (24): "Expected 1 Was -628591228"

  Test: test_swap_NULL_2
  At line (49): "Expected 1 Was -628591228"

  Test: test_swap_behavior_1
  At line (73): "Expected 0 Was -628591232"

--------------------
OVERALL TEST SUMMARY
--------------------
TESTED:  3
PASSED:  0
FAILED:  3
IGNORED: 0

---------------------
BUILD FAILURE SUMMARY
---------------------
Unit test failures.

Alle Tests schlagen wie erwartet fehl und auch die Ausgabe Stop << ist nicht zu sehen, da das Ende des Testes nicht erreicht wurde.

3. Implementierung der Funktionalität

Schlagen alle Tests fehl, so ist die Zeit gekommen um die eigentliche Funktion zu implementieren.

test_swap.c

#include <stdio.h>
#include "swap.h"

int swap(int *key1, int *key2)
{
    if (!key1 || !key2) {
        perror("NULL pointer exception");
        return EXIT_FAILURE;
    }

    int temp = *key1;
    *key2 = *key1;
    *key1 = temp;

    return EXIT_SUCCESS;
}

4. Implementation Prüfen

Die erste Implementation wird mit den geschriebenen Unit-Tests geprüft.

$ ceedling


Test 'test_swap.c'
------------------
Generating runner for test_swap.c...
Compiling test_swap_runner.c...
Compiling test_swap.c...
Compiling swap.c...
Linking test_swap.out...
Running test_swap.out...

-----------
TEST OUTPUT
-----------
[test_swap.c]
  - "NULL pointer exception: Success"
  - "NULL pointer exception: Invalid argument"
  - "NULL pointer exception: Invalid argument"
  - "Test Function: [test_swap_NULL_1]"
  - "Setup =="
  - "Start >>"
  - "Stop <<"
  - ""
  - ""
  - "Test Function: [test_swap_NULL_2]"
  - "Setup =="
  - "Start >>"
  - "Stop <<"
  - ""
  - ""
  - "Test Function: [test_swap_behavior_1]"
  - "Setup =="
  - "Start >>"

-------------------
FAILED TEST SUMMARY
-------------------
[test_swap.c]
  Test: test_swap_behavior_1
  At line (73): "Expected 0 Was 1"

--------------------
OVERALL TEST SUMMARY
--------------------
TESTED:  3
PASSED:  2
FAILED:  1
IGNORED: 0

---------------------
BUILD FAILURE SUMMARY
---------------------
Unit test failures.

Der letzte Test schlägt fehl. Mit Hilfe der Zeilennummer kann geprüft werden welches Makro genau den Fehlschlag verursacht hat. In diesem Fall ist es die folgende Zeile:

TEST_ASSERT_EQUAL(EXIT_SUCCESS, swap(&key_1, NULL));

Durch Unachtsamkeit beim Kopieren wurde das zweite Argument nicht angepasst.

5. Test Korrigieren

Für die Überprüfung der Funktionalität werden zwei gültige Referenzen benötigt. Der Test wird dementsprechend angepasst.

TEST_ASSERT_EQUAL(EXIT_SUCCESS, swap(&key_1, &key_2));

6. Implementation Prüfen

Nach der Anpassung des Testes wird die Implementation erneut getestet.

$ ceedling


Test 'test_swap.c'
------------------
Generating runner for test_swap.c...
Compiling test_swap_runner.c...
Compiling test_swap.c...
Linking test_swap.out...
Running test_swap.out...

-----------
TEST OUTPUT
-----------
[test_swap.c]
  - "NULL pointer exception: Success"
  - "NULL pointer exception: Invalid argument"
  - "Test Function: [test_swap_NULL_1]"
  - "Setup =="
  - "Start >>"
  - "Stop <<"
  - ""
  - ""
  - "Test Function: [test_swap_NULL_2]"
  - "Setup =="
  - "Start >>"
  - "Stop <<"
  - ""
  - ""
  - "Test Function: [test_swap_behavior_1]"
  - "Setup =="
  - "Start >>"

-------------------
FAILED TEST SUMMARY
-------------------
[test_swap.c]
  Test: test_swap_behavior_1
  At line (76): "Expected 43 Was 42"

--------------------
OVERALL TEST SUMMARY
--------------------
TESTED:  3
PASSED:  2
FAILED:  1
IGNORED: 0

---------------------
BUILD FAILURE SUMMARY
---------------------
Unit test failures.

Der Test schlägt erneut fehl. Jedoch zeigt die Zeilennummer bereits, dass es sich nicht um das selbe Makro innerhalb des Testes handelt. In diesem Fall liegt der Fehler bei der Implementation.

7. Implementation Korrigieren

swap.c

#include <stdio.h>
#include <stdlib.h>

#include "swap.h"

int swap(int *key1, int *key2)
{
    if (!key1 || !key2) {
        perror("NULL pointer exception");
        return EXIT_FAILURE;
    }

    int temp = *key1;
    *key2 = *key1;
    *key1 = temp;

    return EXIT_SUCCESS;
}

Das Tauschen der Werte wurde nicht korrekt implementiert. Nach einer Anpassung sieht die Implementation wie folgt aus.

swap.c

#include <stdio.h>
#include <stdlib.h>

#include "swap.h"

int swap(int *key1, int *key2)
{
    if (!key1 || !key2) {
        perror("NULL pointer exception");
        return EXIT_FAILURE;
    }

    int temp = *key1;
    *key1 = *key2;
    *key2 = temp;

    return EXIT_SUCCESS;
}

8. Implementation Prüfen

Nach der Anpassung der Implementation kann nun ein weiterer Testlauf erfolgen.

$ ceedling


Test 'test_swap.c'
------------------
Compiling swap.c...
Linking test_swap.out...
Running test_swap.out...

-----------
TEST OUTPUT
-----------
[test_swap.c]
  - "NULL pointer exception: Success"
  - "NULL pointer exception: Invalid argument"
  - "Test Function: [test_swap_NULL_1]"
  - "Setup =="
  - "Start >>"
  - "Stop <<"
  - ""
  - ""
  - "Test Function: [test_swap_NULL_2]"
  - "Setup =="
  - "Start >>"
  - "Stop <<"
  - ""
  - ""
  - "Test Function: [test_swap_behavior_1]"
  - "Setup =="
  - "Start >>"
  - "Stop <<"
  - ""
  - ""

--------------------
OVERALL TEST SUMMARY
--------------------
TESTED:  3
PASSED:  3
FAILED:  0
IGNORED: 0

Nun sind alle Tests erfolgreich. Der eigentliche Prozess ist damit abgeschlossen. In einem weiteren Schritt kann nun die Implementation überarbeitet (refactoring) werden oder es werden weitere Anforderungen gestellt.