Testing C Code

mu::tiny supports writing tests in pure C. Because the test runner itself is C++, you need two files per test group: a C file containing the tests, and a C++ wrapper that registers them with the C++ runner.

The Two-File Pattern

The C test file

Include “mu/tiny/test.h” and use the C macros:

#include "hello.h"

#include "mu/tiny/test.h"

static const char* output_ptr;
static int (*saved_print)(const char*, ...);

static int capture_output(const char* format, ...)
{
  output_ptr = format;
  return 1;
}

TEST_GROUP_SETUP(hello)
{
  saved_print = print_formated;
  print_formated = capture_output;
  output_ptr = NULL;
}

TEST_GROUP_TEARDOWN(hello)
{
  print_formated = saved_print;
}

TEST(hello, PrintsHelloWorld)
{
  print_hello_world();
  CHECK_EQUAL_STRING("Hello World!\n", output_ptr);
}

TEST_GROUP_SETUP and TEST_GROUP_TEARDOWN in C define wrapper functions that the C++ file calls. TEST in C defines a wrapper function body.

The C++ registration wrapper

#include "mu/tiny/test.hpp"

TEST_GROUP_C_WRAPPER(hello)
{
  TEST_GROUP_C_SETUP_WRAPPER(hello)
  TEST_GROUP_C_TEARDOWN_WRAPPER(hello)
};

TEST_C_WRAPPER(hello, PrintsHelloWorld)

Both files must be compiled together into the same test executable:

add_executable(my_tests main.cpp foo_test.c foo_test.cpp)
target_link_libraries(my_tests PRIVATE mu::tiny)

C Assertion Macros (mu/tiny/test.h)

The C header provides typed macros because C does not have overloaded functions:

Each macro also has a _TEXT variant that accepts an extra message string.

C Test Definition Macros

Macro

Purpose

TEST_GROUP_SETUP(group)

Defines the setup function for group

TEST_GROUP_TEARDOWN(group)

Defines the teardown function for group

TEST(group, name)

Defines a test function body

SKIPPED_TEST(group, name)

Defines an skipped test body

XFAIL_TEST(group, name)

Defines an expected-failure test body

C++ Wrapper Macros

Each C macro that defines a test body has a matching C++ wrapper:

C++ wrapper

What it creates

TEST_GROUP_C_WRAPPER(group)

A TEST_GROUP with optional setup/teardown wiring

TEST_GROUP_C_SETUP_WRAPPER(group)

Calls the C setup function from C++ setup()

TEST_GROUP_C_TEARDOWN_WRAPPER(group)

Calls the C teardown function from C++ teardown()

TEST_C_WRAPPER(group, name)

A TEST that calls the C test function

SKIPPED_TEST_C_WRAPPER(group, name)

An SKIPPED_TEST that calls the C skipped function

XFAIL_TEST_C_WRAPPER(group, name)

An XFAIL_TEST that calls the C expected-fail function

C Mock Interface (mu/tiny/mock.h)

struct MutinyMockSupport
[source]

Vtable of function pointers that form the C mock API. Obtain a pointer via mutiny_mock() or mutiny_mock_scope().

MutinyMockSupport *mutiny_mock(void)
[source]

Returns the global MutinyMockSupport instance.

MutinyMockSupport *mutiny_mock_scope(const char *name)
[source]

Returns the named MutinyMockSupport scope.

The C mock API exposes MutinyMockSupport through mutiny_mock(), which returns a pointer to a struct of function pointers. This allows chaining calls in C99:

#include "mu/tiny/mock.h"

/* expect */
mutiny_mock()->expect_one_call("send")
    ->with_int_parameters("fd", 3)
    ->and_return_int_value(8);

/* actual (in stub) */
int n = (int)mutiny_mock()
    ->actual_call("send")
    ->with_int_parameters("fd", fd)
    ->int_return_value();

For named scopes: mutiny_mock_scope("name") returns a pointer to the named MutinyMockSupport.

Data store in C

mutiny_mock()->set_int_data("retval", 42);
int v = mutiny_mock()->get_data("retval").value.int_value;

Custom comparators in C

static int my_equal(const void* a, const void* b) { return a == b; }
static const char* my_to_string(const void* a) { (void)a; return "obj"; }

mutiny_mock()->install_comparator("MyType", my_equal, my_to_string);

Complete C mock example:

#include "mu/tiny/mock.h"
#include "mu/tiny/test.h"

static int equal_method(const void* object1, const void* object2)
{
  return object1 == object2;
}

static const char* to_string_method(const void* object)
{
  (void)object;
  return "string";
}

TEST(MockDocumentation_C, CInterface)
{
  void* object = (void*)0x1;

  mutiny_mock()
      ->expect_one_call("foo")
      ->with_int_parameters("integer", 10)
      ->and_return_double_value(1.11);
  double d = mutiny_mock()
                 ->actual_call("foo")
                 ->with_int_parameters("integer", 10)
                 ->return_value()
                 .value.double_value;
  CHECK_EQUAL_DOUBLE(1.11, d, 0.00001);

  mutiny_mock()->install_comparator("type", equal_method, to_string_method);
  mutiny_mock_scope("scope")->expect_one_call("bar")->with_parameter_of_type(
      "type", "name", object
  );
  mutiny_mock_scope("scope")->actual_call("bar")->with_parameter_of_type(
      "type", "name", object
  );
  mutiny_mock()->remove_all_comparators_and_copiers();

  mutiny_mock()->set_int_data("important", 10);
  mutiny_mock()->check_expectations();
  mutiny_mock()->clear();
}

Examples

Files

Demonstrates

hello.test.c + hello.test.cpp

Two-file pattern: stubs a function pointer with MUTINY_PTR_SET, checks output with CHECK_EQUAL_STRING

MockDocumentation.test.c + MockDocumentation.test.cpp

C mock interface: mutiny_mock(), typed parameters, custom comparator, data store