tdd-in-c/slides/TDD in C.md

6.8 KiB

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:

int add(int a, int b);

src/my_fancy_calculator.c:

#include "my_fancy_calculator.h"

int add(int a, int b) {
  return a - b;
}

Unit tests with cgreen

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

#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


Mocks in cgreen

#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

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

#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

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


Some disclaimers

  • cgreen's buildsystem is CMAKE
  • meson usage with cgreen-runner does not work out of the box
  • 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