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ürEXIT_SUCCESS
undEXIT_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 FunktionNULL
.test_swap_NULL_2
übergibt für den zweiten Parameter der FunktionNULL
.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.