Assert C or Assertic. A simple, lightweight test framework for C inspired by Rust's built-in testing system. Tests are automatically discovered and registered using a simple macro, making it easy to write and run tests.
- Automatic test discovery: Tests are registered automatically using the
TEST()
macro - Rust-like syntax: Clean, minimal syntax similar to Rust's
#[test]
attribute - Simple assertions:
assert_eq()
,assert_ne()
, andassert()
macros - Colored output: Green for passing tests, red for failures
- Zero configuration: Just write tests and run - no manual test registration needed
- Lightweight: Minimal overhead and dependencies
Use the TEST()
macro to define a test function:
#include <assertc.h>
TEST(my_test_name) {
// Test code here
assert_eq(5, 2 + 3);
assert_ne(4, 2 + 3);
assert(true);
}
TEST(another_test) {
int result = some_function();
assert_eq(42, result);
}
int main(void) {
return run_tests(); // Runs all registered tests
}
The framework provides three main assertion macros:
assert_eq(expected, actual)
- Assert that two values are equalassert_ne(not_expected, actual)
- Assert that two values are not equalassert(condition)
- Assert that a condition is true
running 3 tests
test my_test_name ... ok
test another_test ... ok
test failing_test ... x assert_eq failed: expected 5, got 4
FAILED
test result: FAILED. 2 passed; 1 failed
Look at the tests/
directory for more examples.
example_test_fail.c
is excluded from Makefile, to pass build.
Compile your test file with the framework:
clang -Wall -Iinclude -o my_test my_test.c src/assertc.c [other_sources...]
./my_test
- Include the header file:
#include <assertc.h>
- Link with the test framework source:
src/assertc.c
- Compile with the include directory:
-Iinclude
Add the test framework to your test dependencies:
# Makefile
TEST_FRAMEWORK = $(SRCDIR)/assertc.c
TEST_DEPS_your_test = $(SRCDIR)/your_module.c $(assertc)
- Organize tests by functionality - Group related tests together
- Use descriptive test names - Make it clear what each test is checking
- Test edge cases - Include boundary conditions, empty inputs, etc.
- Test both positive and negative cases - Verify expected behavior and error handling
- Keep tests independent - Each test should be able to run in isolation
- Use consistent test data - Define test arrays as static variables for reuse
int main() {
test_suite_start("My Tests");
test_case("Test 1");
assert_eq(expected, actual);
test_case("Test 2");
assert_eq(expected, actual);
test_suite_end();
}
TEST(test_1) {
assert_eq(expected, actual);
}
TEST(test_2) {
assert_eq(expected, actual);
}
int main() {
return run_tests(); // Automatically finds and runs all tests
}
- Less boilerplate: No need to manually register tests
- Cleaner organization: Each test is a separate function
- Automatic discovery: Tests are found automatically at compile time
- Familiar syntax: Similar to modern testing frameworks (Rust, Go)
- Simple main(): Just call
run_tests()
and you're done
- Tests are registered automatically using the
__attribute__((constructor))
- Test registry can hold up to
256
tests (easily configurable) - Test functions are called in the order they were registered
- Return code is
0
for success,1
for any failures - Thread-safe (tests run sequentially)
The framework relies on __attribute__((constructor))
for automatic test registration, which is a GNU extension supported by:
- ✅ GCC (GNU Compiler Collection)
- ✅ Clang (aims for GCC compatibility)
- ❌ Microsoft Visual C++ (MSVC)
- ❌ Some proprietary compilers in strict standards mode
- ❌ Many embedded system compilers
Portability Problems: Code will not compile on MSVC without modifications. For Windows development, you'll need conditional compilation or alternative implementations.
Initialization Order: Constructor functions run in undefined order relative to each other. Multiple test files may register tests unpredictably, though this rarely affects test functionality.
Debugging Difficulties: Since constructors run before main()
, crashes during test registration can be harder to debug with limited stack trace information.
Static Initialization Fiasco: If test registration depends on other global variables, you may encounter initialization order issues across translation units.
For maximum portability, consider:
- Using
#ifdef
guards with compiler-specific alternatives - Manual test registration (less convenient but universally compatible)
- Build system code generation for registration
The trade-off is between API elegance and compiler compatibility.