Cross-platform, heap-free C test framework with lifecycle hooks and parameterized functions, assertions with ad-hoc formatting and detailed failure diagnostics.
- Automatic Test Registration: Attest automatically discover tests.
- Parameterized Testing: Reduce boilerplate by running the same test logic against different data sets.
- Lifecycle Management: Includes
setupandteardownhooks 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.
#include "attest.h"
TEST(math) {
int expected = 7;
int actual = 3 + 4;
EXPECT_EQ(actual, expected);
}Drop the header file anywhere in your project's include directory. Then include it like so:
#include "attest.h"Note on naming, all macros have lowercase versions.
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.
| Option | Type | Default | Description |
|---|---|---|---|
.disabled |
bool |
false |
If true, the runner ignores the test and doesn't report |
.skip |
bool |
false |
If true, the runner ignores the test but still report it |
.attempts |
int |
1 |
The number of times to execute the test body |
.tags |
char*[ATTEST_MAX_TAGS] |
NULL |
tags associated with the function. |
.before |
void(*)(TextContext*) |
NULL |
A function that runs before the test. |
.after |
void(*)(TextContext*) |
NULL |
A function that runs after the test. |
Example:
TEST(hit_api, .attempts = 10, .tags = { "slow" }) {
int result = handle_api_request();
EXPECT_EQ(result, 200);
}Defines a test case that accepts a Context object.
Parameters:
name: Unique name for the test case. No spaces or quotestest_context: Has typeTestContextand 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);
}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:
| name | Type | Description |
|---|---|---|
all |
void* |
Data shared amongst all tests and lifecycle functions. The value is available for the entire program. |
each |
void* |
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. |
self |
void* |
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);
}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:
| name | Type | Description |
|---|---|---|
all |
void* |
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;
}A lifecycle function that runs before all tests and lifecycle functions.
Parameters:
test_context: Has typeTestContextand allows sharing custom data.
Example:
BEFORE_ALL(test_context)
{
int* foo = malloc(sizeof(int));
*foo = 7;
test_context->shared = (void*)foo;
}A lifecycle function that runs before each test.
Parameters:
test_context: Has typeTestContextand 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;
}A lifecycle function that runs after each tests.
Parameters:
test_context: Has typeTestContextand allows sharing custom data.
Example:
AFTER_EACH(test_context)
{
free(test_context->local);
test_context->local = NULL;
}A lifecycle function that runs after all tests and lifecycle functions.
Parameters:
test_context: Has typeTestContextand allows sharing custom data.
Example:
AFTER_ALL(test_context)
{
free(test_context->shared);
}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; }wherenameis an optional name for the test case anddatais the value to passed to the test.
Options:
Accepts all the options available to TEST this macro accepts:
| Option | Type | Description |
|---|---|---|
.before_all_cases |
void(*)(ParamContext*) |
A test that runs before all cases. |
.after_all_cases |
void(*)(ParamContext*) |
A test that runs after all cases. |
.before_each_case |
void(*)(ParamContext*) |
A test that runs before each case. |
.after_each_cases |
void(*)(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);
}Parameters:
name: Unique name for the parameterized test. No spaces or quotes.param_context: Name ofParamContext.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; }wherenameis an optional name for the test case anddatais 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);
}Attest passes a ParamContext object to each test case of a parameterized tests and each parameterized lifecycle function.
Fields:
| name | Type | Description |
|---|---|---|
all |
void* |
Data intended shared amongst all tests and lifecycle functions. Value available for entire program. |
set |
void* |
Data intended for entire parameterized test. Set by .before_all_cases. |
self |
void* |
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);
}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:
| Macro | Arguments | Description |
|---|---|---|
| EXPECT(x) | bool |
Confirm true expression. |
| EXPECT_FALSE(x) | bool |
Confirm 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, char |
Confirm same character. |
| EXPECT_DIFF_CHAR(a, b) | char, char |
Confirm 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. |
Macros to change the behavior of Attest. You must define runner options before including attest.h.
Options:
| Macro | Type | Default | Description |
|---|---|---|---|
ATTEST_MAX_TESTS |
int |
128 |
Max number of tests allowed per binary. |
ATTEST_NO_COLOR |
bool |
false |
Disables ANSI color codes in output report. |
ATTEST_NO_UTF8 |
bool |
NULL |
Disables UTF8 output. |
ATTEST_MAX_FAILURES |
int |
16 |
Max amount of failures per test. |
ATTEST_MAX_TEST_ATTEMPTS |
int |
32 |
Max amount of attempts per test. |
ATTEST_MAX_TAGS |
int |
8 |
Max amount of tags per test. |
ATTEST_MAX_TAG_SIZE |
int |
21 |
Max tag size. |
ATTEST_VALUE_BUF |
int |
128 |
The max size of buffer used in failure messages. |
ATTEST_MAX_PARAMERTERIZE_RESULTS |
int |
32 |
The max amount of failures for a parameterize test. |
ATTEST_CASE_NAME_SIZE |
int |
128 |
The max size for the case name of a parameterize test. |
Example:
#define ATTEST_NO_COLOR 1
#include "attest.h"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)
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.
Normal Tests:
BEFORE_ALLBEFORE_EACHTESTorTEST_CTXAFTER_EACHAFTER_ALL
Parameterized Tests:
BEFORE_ALLBEFORE_ALL_CASESBEFORE_EACH_CASEPARAM_TESTorPARAM_TEST_CTXAFTER_EACH_CASEAFTER_ALL_CASESAFTER_ALL