# Test Driven Development in C --- ### TDD recap ![[TDD cycle.excalidraw]] * good for unit tests, if requirements are known (specification exists) --- ### cgreen testing framework * readable * mocking support * different test reporters * align nicely in CI (`cobertura`/`xJunit`) * user friendly (`pretty`) * test discovery (`cgreen-runner`) --- ### Example: "Calculator" `inc/my_fancy_calculator.h`: ```c int add(int a, int b); ``` `src/my_fancy_calculator.c`: ```c #include "my_fancy_calculator.h" int add(int a, int b) { return a - b; } ``` --- ### Unit tests with cgreen `test/my_fancy_test.c`: ```c #include #include Ensure(my_fancy_calculator_can_do_addition) { assert_that(add(2, 3), is_equal_to(5)); } ``` --- * automatic test discovery `cgreen_runner libdemo_test.so`: ``` Running "libdemo_test" (1 test)... ../test/my_fancy_test.c:5: Failure: default -> my_fancy_calculator_can_do_addition Expected [add(2, 3)] to [equal] [5] actual value: [-1] expected value: [5] "default": 1 failure in 30ms. Completed "libdemo_test": 1 failure in 30ms. ``` --- ### BDD Behavioral Driven Development * isolated tests * via mocking * SDKs * Network * etc * via test fixtures (`BeforeEach`/`AfterEach`) * context setup and tear down * good for reproducing bugs --- ### Example: MQTT client ```c #include "mqtt_example.h" #include #include #include #include #define ADDRESS "tcp://localhost:1883" #define CLIENTID "ExampleClientPub" #define TOPIC "MQTT Examples" #define QOS 1 #define TIMEOUT 10000L int send_mqtt_msg(char *payload, size_t payload_len) { MQTTClient client; MQTTClient_connectOptions conn_opts = MQTTClient_connectOptions_initializer; MQTTClient_message pubmsg = MQTTClient_message_initializer; MQTTClient_deliveryToken token; int rc; MQTTClient_create(&client, ADDRESS, CLIENTID, MQTTCLIENT_PERSISTENCE_NONE, NULL); conn_opts.keepAliveInterval = 20; conn_opts.cleansession = 1; if ((rc = MQTTClient_connect(client, &conn_opts)) != MQTTCLIENT_SUCCESS) { printf("Failed to connect, return code %d\n", rc); exit(-1); } pubmsg.payload = payload; pubmsg.payloadlen = payload_len; pubmsg.qos = QOS; pubmsg.retained = 0; MQTTClient_publishMessage(client, TOPIC, &pubmsg, &token); printf("Waiting for up to %d seconds for publication of %s\n" "on topic %s for client with ClientID: %s\n", (int)(TIMEOUT/1000), payload, TOPIC, CLIENTID); rc = MQTTClient_waitForCompletion(client, token, TIMEOUT); printf("Message with delivery token %d delivered\n", token); MQTTClient_disconnect(client, 10000); MQTTClient_destroy(&client); return rc; } ``` [Eclipse Paho](https://www.eclipse.org/paho/index.php?page=clients/c/index.php) --- ### Mocks in cgreen ```c #include #include LIBMQTT_API int MQTTClient_create(MQTTClient* handle, const char* serverURI, const char* clientId, int persistence_type, void* persistence_context) { return (int)mock(handle); } LIBMQTT_API int MQTTClient_connect(MQTTClient handle, MQTTClient_connectOptions* options) { return (int)mock(handle); } LIBMQTT_API int MQTTClient_publishMessage(MQTTClient handle, const char* topicName, MQTTClient_message* msg, MQTTClient_deliveryToken* dt) { return (int)mock(msg->payload, msg->payloadlen); } LIBMQTT_API int MQTTClient_waitForCompletion(MQTTClient handle, MQTTClient_deliveryToken dt, unsigned long timeout) { return (int)mock(); } LIBMQTT_API int MQTTClient_disconnect(MQTTClient handle, int timeout) { return (int)mock(); } LIBMQTT_API void MQTTClient_destroy(MQTTClient* handle) { mock(handle); } ``` --- ### BDD-style unit test with cgreen ```c Describe(mqtt_example); BeforeEach(mqtt_example) {} AfterEach(mqtt_example) {} Ensure(mqtt_example, payload_is_sent) { // Arrange MQTTClient mock_handle = NOT_NULL; char *payload = "Hello World."; size_t payload_len = strlen(payload); // Expect expect(MQTTClient_create, will_set_contents_of_parameter(handle, &mock_handle, sizeof(mock_handle))); expect(MQTTClient_connect, when(handle, is_equal_to(mock_handle)), will_return(MQTTCLIENT_SUCCESS)); expect(MQTTClient_publishMessage, when(msg->payload, is_equal_to_string(payload)), when(msg->payloadlen, is_equal_to(payload_len))); expect(MQTTClient_waitForCompletion, will_return(MQTTCLIENT_SUCCESS)); expect(MQTTClient_disconnect); expect(MQTTClient_destroy); // Act send_mqtt_msg(payload, payload_len); } ``` --- ### cgreen learning mock mode ```c #include #include // [mock definitions] Describe(mqtt_example); BeforeEach(mqtt_example) { cgreen_mocks_are(learning_mocks); } AfterEach(mqtt_example) {} Ensure(mqtt_example, payload_is_sent) { // Arrange char *payload = "Hello World."; size_t payload_len = strlen(payload); // Expect // TODO // Act send_mqtt_msg(payload, payload_len); } ``` --- ### learned mocks ```c stdout: Running "libmqtt_example_test" (1 test)... Waiting for up to 10 seconds for publication of Hello World. on topic MQTT Examples for client with ClientID: ExampleClientPub Message with delivery token 0 delivered "mqtt_example": No assertions. Completed "libmqtt_example_test": No assertions. stderr: mqtt_example -> payload_is_sent : Learned mocks are expect(MQTTClient_create, when(handle, is_equal_to(140735561153976))); expect(MQTTClient_connect, when(handle, is_equal_to(0))); expect(MQTTClient_publishMessage, when(msg->payload, is_equal_to(139884358905951)), when(msg->payloadlen, is_equal_to(12))); expect(MQTTClient_waitForCompletion); expect(MQTTClient_disconnect); expect(MQTTClient_destroy, when(handle, is_equal_to(140735561153976))); ``` --- ### Whats next? Try it out! ![cgreen logo](https://github.com/cgreen-devs/cgreen/raw/master/doc/logo.png?s=300) * [cgreen tutorial](https://cgreen-devs.github.io/cgreen/cgreen-guide-en.html) * [cgreen repo](https://github.com/cgreen-devs/cgreen) * [cgreen cheat sheet](https://cgreen-devs.github.io/cgreen/cheat-sheet.html) --- ### Some disclaimers * cgreen's buildsystem is `CMAKE` * `meson` usage with `cgreen-runner` does not work out of the box * see [repo of this talk](https://git.dezentrale.cloud/oniboni/tdd-in-c.git) for example setup * but.. cgreen is availabe on most common linux distros (`-lcgreen`, `find_program`, etc should work just fine) --- ### [backup] TDD test structure * arrange * act -- test function * assert --- ### [backup] BDD test structure * arrange (via test fixtures) * expect -- setup behavior of mocks * act -- entry function to trigger behavior * (assert) -- check post conditions