Home

Doctests in C

David Priver, July 30th, 2022

Doctests are a technique of testing the examples embedded in documentation. They were first created for the Python language in 1999, but have since spread to other languages like Rust.

Embedding examples in documentation is very useful as it can immediately clarify usage of an API or the meaning of unclear terms. However, they can rot as the API changes, which can be worse than having nothing at all. What are we to do?

Well, as usual in C when you have a problem that can't be solved directly in the language itself, you reach for the preprocessor (this usage isn't that crazy, I promise!). Just include the examples in the doc, but guard them with an #ifdef. You can then write a test program that #includes the documentation file, but with that macro defined so the examples are actual code. Then, run the tests!

An example:

// some_api.h
#include <stddef.h> // size_t
typedef struct SomeApiContext SomeApiContext;

// Visibility modifiers, dllexport, and some documentation
// elided for brevity.

SomeApiContext* some_api_create(void);
// ...

void some_api_store_data(SomeApiContext*, int*, size_t);
// ...

size_t
some_api_get_data(
    SomeApiContext* ctx,
    int* buff, size_t bufflen,
    size_t* cookie);
// Copies the data into a user provided buffer.
//
// Arguments:
// ----------
// ctx:
//      The api context
// buff:
//      The buffer to copy the data into.
// bufflen:
//      The length (in items, not bytes) of buff.
// cookie:
//      A pointer to an opaque value for remembering where
//      in the data this function is. Initialize cookie to 0
//      before calling this function.
// 
// Returns:
// --------
// The number of items copied into buff. If 0 is returned,
// no items were copied into the buff and there are no more
// items to copy.
//
// Example:
// --------
#ifdef SOME_API_EXAMPLE
int sum_some_api_data(SomeApiContext* ctx){
    int result = 0;
    enum {buff_len=32};
    int buff[buff_len];
    size_t n = 0;
    size_t cookie = 0;
    while((n = some_api_get_data(ctx, buff, buff_len, &cookie))){
        for(size_t i = 0; i < n; i++){
            result += buff[i];
        }
    }
    return result;
}
#endif // SOME_API_EXAMPLE

And the test code:

// test_some_api_docs.c
#include <assert.h>
#define SOME_API_EXAMPLE
#include "some_api.h"

int main(void){
    int data[3] = {1, 2, 3};
    SomeApiContext* ctx = some_api_create();
    some_api_store_data(ctx, data, 3);
    assert(sum_some_api_data(ctx) == 6);
    return 0;
}

This isn't as nice as automatically extracting the examples like doctest can do, but when integrated into your test suite you will no longer have regressions in your documentation's examples. Additionally, it is nice to have examples right next to the definitions in the header anyway as that is what you'll jump to when you use your editor's GoToDefinition functionality. Finally, it is easier to write the examples as the examples are written as actual code instead of embedded in comments (so your editor and other tools understand it as code).

You do need a documentation generator which understands that it needs to include these #ifdef'd example blocks. I'm working on such a tool based on libclang, but it is not ready for release yet.

All code in this article is released into the public domain.