270 lines
6.8 KiB
Markdown
270 lines
6.8 KiB
Markdown
|
# 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 <cgreen/cgreen.h>
|
||
|
#include <my_fancy_calculator.h>
|
||
|
|
||
|
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 <stdio.h>
|
||
|
#include <stdlib.h>
|
||
|
#include <string.h>
|
||
|
#include <MQTTClient.h>
|
||
|
|
||
|
#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 <MQTTClient.h>
|
||
|
#include <cgreen/mocks.h>
|
||
|
|
||
|
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 <cgreen/cgreen.h>
|
||
|
#include <mqtt_example.h>
|
||
|
|
||
|
// [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
|
||
|
|
||
|
|