Attest

February 24, 2026 ยท View on GitHub

Cross-platform, heap-free C test framework with lifecycle hooks and parameterized functions, assertions with ad-hoc formatting and detailed failure diagnostics.

Features

  • Automatic Test Registration: Attest automatically discover tests.
  • Parameterized Testing: Reduce boilerplate by running the same test logic against different data sets.
  • Lifecycle Management: Includes setup and teardown hooks with context-passing.
  • Test Categorization: Use tags to organize your suite, allowing you to filter groups of tests.
  • Rich Assertions: expect-style assertions with support for ad-hoc formatting for descriptive messages.
  • Zero Dynamic Allocation: Performs no heap allocation. It operates on static storage.
  • Fine-Grained Orchestration: Built-in support for skipping or retrying tests to handle any environment.
  • Lightweight & Cross-Platorm: Supports Windows, MacOS and Linux with a minimal memory footprint.

Basic usage

#include "attest.h"

TEST(math) {
    int expected = 7;
    int actual = 3 + 4;

    EXPECT_EQ(actual, expected);
}

Installation

Drop the header file anywhere in your project's include directory. Then include it like so:

#include "attest.h"

API

Note on naming, all macros have lowercase versions.

TEST(name, [options...])

Defines a test case. This macro automatically registers the test case with the Attest.

Parameters:

  • name: Unique name for the test case. No spaces or quotes allowed.

Options: The options may appear in any order. You pass the options as arguments prefixed with a dot.

OptionTypeDefaultDescription
.disabledboolfalseIf true, the runner ignores the test and doesn't report
.skipboolfalseIf true, the runner ignores the test but still report it
.attemptsint1The number of times to execute the test body
.tagschar*[ATTEST_MAX_TAGS]NULLtags associated with the function.
.beforevoid(*)(TextContext*)NULLA function that runs before the test.
.aftervoid(*)(TextContext*)NULLA function that runs after the test.

Example:

TEST(hit_api, .attempts = 10, .tags = { "slow" }) {
    int result = handle_api_request();
    EXPECT_EQ(result, 200);
}

TEST_CTX(name, test_context, [options...])

Defines a test case that accepts a Context object.

Parameters:

  • name: Unique name for the test case. No spaces or quotes
  • test_context: Has type TestContext and allows sharing allocated data.

Options: Accept all the options available to TEST.

Example:

#include "attest.h"

BEFORE_ALL(test_context)
{
    int* foo = malloc(sizeof(int));
    *foo = 7;
    test_context->shared = (void*)foo;
}

TEST_CTX(with_a_context, test_context)
{
    int global_num = *(int*)test_context->shared;

    EXPECT_EQ(14 + global_num, 21);
}

TestContext

Attest passes a TestContext object to each lifecycle function and TEST_CTX function. This object has fields containing user custom data. User's responsibility to clean up data.

Fields:

nameTypeDescription
allvoid*Data shared amongst all tests and lifecycle functions. The value is available for the entire program.
eachvoid*Data created for each test. This field is freed after each test and should be set for each test inside of the before_each lifecycle function.
selfvoid*Data intended for a single test. This field will be freed at the end of a test and should be set inside of a .before lifecycle function.

Example:

#include "attest.h"

BEFORE_ALL(test_context)
{
    int* foo = malloc(sizeof(int));
    *foo = 7;
    test_context->shared = (void*)foo;
}

AFTER_ALL(test_context)
{
    free(test_context->shared);
}

TEST_CTX(with_a_context, test_context)
{
    int global_num = *(int*)test_context->shared;

    EXPECT_EQ(14 + global_num, 21);
}

GlobalContext

Attest passes a GlobalContext object to BEFORE_ALL and AFTER_ALL lifecycle function. This object has fields containing user custom data. User's responsibility to clean up data.

Fields:

nameTypeDescription
allvoid*Data shared amongst all tests and lifecycle functions. The value is available for the entire program.

Example:

#include "attest.h"

BEFORE_ALL(test_context)
{
    int* foo = malloc(sizeof(int));
    *foo = 7;
    test_context->shared = (void*)foo;
}

BEFORE_ALL(test_context)

A lifecycle function that runs before all tests and lifecycle functions.

Parameters:

  • test_context: Has type TestContext and allows sharing custom data.

Example:

BEFORE_ALL(test_context)
{
    int* foo = malloc(sizeof(int));
    *foo = 7;
    test_context->shared = (void*)foo;
}

BEFORE_EACH(test_context)

A lifecycle function that runs before each test.

Parameters:

  • test_context: Has type TestContext and allows sharing custom data.

Example:

BEFORE_EACH(test_context)
{
    int* foo = malloc(sizeof(int));
    *foo = *(int*)test_context->shared - 3;
    test_context->local = (void*)foo;
}

AFTER_EACH(test_context)

A lifecycle function that runs after each tests.

Parameters:

  • test_context: Has type TestContext and allows sharing custom data.

Example:

AFTER_EACH(test_context)
{
    free(test_context->local);
    test_context->local = NULL;
}

AFTER_ALL(test_context)

A lifecycle function that runs after all tests and lifecycle functions.

Parameters:

  • test_context: Has type TestContext and allows sharing custom data.

Example:

AFTER_ALL(test_context)
{
    free(test_context->shared);
}

PARAM_TEST(name, case_type, case_name, (values), [options...])

Parameters:

  • name: Unique name for the parameterized test. No spaces or quotes.
  • case_type: Case data type.
  • case_name: Name of case data.
  • values: List of structures enclosed in parenthesis of type { char[ATTEST_CASE_NAME_SIZE] name; case_type data; } where name is an optional name for the test case and data is the value to passed to the test.

Options: Accepts all the options available to TEST this macro accepts:

OptionTypeDescription
.before_all_casesvoid(*)(ParamContext*)A test that runs before all cases.
.after_all_casesvoid(*)(ParamContext*)A test that runs after all cases.
.before_each_casevoid(*)(ParamContext*)A test that runs before each case.
.after_each_casesvoid(*)(ParamContext*)A test that runs after each case.

Example:

#include "attest.h"

PARAM_TEST(fruit_basket,
    int,
    case_num,
    ({ 1, "one" } , { 2, "two" } , { 3, "three" }))
{
    EXPECT_EQ(case_num, 1);
}

PARAM_TEST_CTX(name, param_contest, case_type, case_name, (values), [options...])

Parameters:

  • name: Unique name for the parameterized test. No spaces or quotes.
  • param_context: Name of ParamContext.
  • case_type: case data type.
  • case_name: Name of case data.
  • values: a list of structures enclosed in parenthesis of type { char[ATTEST_CASE_NAME_SIZE] name; case_type data; } where name is an optional name for the test case and data is the value to passed to the test.

Options: Accepts all the options available to PARAM_TEST and TEST.

Example:

#include "attest.h"

PARAM_TEST_CTX(basket_case,
    param_context,
    int,
    case_num,
    ({ 1, "one" } , { 2, "two" } , { 3, "three" }))
{
    int global_num = *(int*)context->shared;
    EXPECT_EQ(global_num, case_num);
}

ParamContext

Attest passes a ParamContext object to each test case of a parameterized tests and each parameterized lifecycle function.

Fields:

nameTypeDescription
allvoid*Data intended shared amongst all tests and lifecycle functions. Value available for entire program.
setvoid*Data intended for entire parameterized test. Set by .before_all_cases.
selfvoid*Data intended for each case. Set by .before_each_case.

Example:

#include "attest.h"

PARAM_TEST_CTX(basket_case,
    param_context,
    int,
    case_num,
    ({ 1, "one" } , { 2, "two" } , { 3, "three" }))
{
    int shared_num = *(int*)param_context->shared;
    EXPECT_EQ(shared_num, case_num);
}

Expectations

Attest only provide expectations. Expectations don't stop the test, attest execute their arguments exactly once and tests can have more than one.

For each expectation, you can pass it variable amount arguments passed to it and used those arguments to create a formatted message.

Example:

TEST(hit_api, .attempts = 10) {
    int expected_status = 200;

    int result = handle_api_request();

    EXPECT_EQ(result, expected_status, "Should return %d, but got %d", expected_status, result);
}
  • EXPECT_: Records a failure but continues the test execution.

List of Expectation:

MacroArgumentsDescription
EXPECT(x)boolConfirm true expression.
EXPECT_FALSE(x)boolConfirm false expression.
EXPECT_SAME_STRING(a, b)char*, char*Confirm same strings.
EXPECT_DIFF_STRING(a, b)char*, char*Confirm different strings.
EXPECT_SAME_CHAR(a, b)char, charConfirm same character.
EXPECT_DIFF_CHAR(a, b)char, charConfirm different character.
EXPECT_SAME_MEMORY(a, b)void*, void*Confirm same memory.
EXPECT_DIFF_MEMORY(a, b)void*, void*Confirm different memory.
EXPECT_SAME_PTR(a, b)*, *Confirm same pointer.
EXPECT_DIFF_PTR(a, b)*, *Confirm different pointer.
EXPECT_NULL(x)*Confirm NULL pointer.
EXPECT_NOT_NULL(x)*Confirm not NULL pointer
EXPECT_EQ(a, b)<any integer>, <any integer>Cast each argument to a long long int and check a == b.
EXPECT_NEQ(a, b)<any integer>, <any integer>Cast each argument to a long long int and check a != b.
EXPECT_LT(a, b)<any integer>, <any integer>Cast each argument to a long long int and check a < b.
EXPECT_LTE(a, b)<any integer>, <any integer>Cast each argument to a long long int and check a <= b.
EXPECT_GT(a, b)<any integer>, <any integer>Cast each argument to a long long int and check a > b.
EXPECT_GTE(a, b)<any integer>, <any integer>Cast each argument to a long long int and check a >= b.
EXPECT_EQ_U(a, b)<any integer>, <any integer>Cast each argument to a unsigned long long int and check a == b.
EXPECT_NEQ_U(a, b)<any integer>, <any integer>Cast each argument to a unsigned long long int and check a != b.
EXPECT_GTE_U(a, b)<any integer>, <any integer>Cast each argument to a unsigned long long int and check a >= b.
EXPECT_GT_U(a, b)<any integer>, <any integer>Cast each argument to a unsigned long long int and check a > b.
EXPECT_LT_U(a, b)<any integer>, <any integer>Cast each argument to a unsigned long long int and check a < b.
EXPECT_LTE_U(a, b)<any integer>, <any integer>Cast each argument to a unsigned long long int and check a <= b.

Runner options

Macros to change the behavior of Attest. You must define runner options before including attest.h.

Options:

MacroTypeDefaultDescription
ATTEST_MAX_TESTSint128Max number of tests allowed per binary.
ATTEST_NO_COLORboolfalseDisables ANSI color codes in output report.
ATTEST_NO_UTF8boolNULLDisables UTF8 output.
ATTEST_MAX_FAILURESint16Max amount of failures per test.
ATTEST_MAX_TEST_ATTEMPTSint32Max amount of attempts per test.
ATTEST_MAX_TAGSint8Max amount of tags per test.
ATTEST_MAX_TAG_SIZEint21Max tag size.
ATTEST_VALUE_BUFint128The max size of buffer used in failure messages.
ATTEST_MAX_PARAMERTERIZE_RESULTSint32The max amount of failures for a parameterize test.
ATTEST_CASE_NAME_SIZEint128The max size for the case name of a parameterize test.

Example:

#define ATTEST_NO_COLOR 1
#include "attest.h"

Attest behavior

Compatibility:

Supports Clang on Windows and GCC/Clang on all *nixes. The team built Attest with Clang on Windows MacOS and Fedora Linux.

On Windows, Attest does not compile with MSVC. Although, Attest is not compatible with MSVC currently but this is on the Roadmap and important to author.

Platform support:

  • *nixes (GCC/Clang)
  • MacOS (GCC/Clang)
  • Windows (Clang)

Undefined behavior policy such as segmentation faults

Attest does not catch or recover from segmentation faults. If the user's code segfaults, the OS terminates the test process immediately, just like any normal C program. Attest does not intercept signals, fork processes, or attempt to continue execution after undefined behavior.

Test execution order:

Normal Tests:

  1. BEFORE_ALL
  2. BEFORE_EACH
  3. TEST or TEST_CTX
  4. AFTER_EACH
  5. AFTER_ALL

Parameterized Tests:

  1. BEFORE_ALL
  2. BEFORE_ALL_CASES
  3. BEFORE_EACH_CASE
  4. PARAM_TEST or PARAM_TEST_CTX
  5. AFTER_EACH_CASE
  6. AFTER_ALL_CASES
  7. AFTER_ALL