CLI tool
sheredom/utest.h avatar
sheredom/utest.h

utest.h: a single header unit testing framework for C and C++

🧪 single header unit testing framework for C and C++

996 stars75 forksC++Unlicense

At a glance

What is it?
utest.h is a unit testing framework that lives entirely in one header file you copy into your project, with UTEST macros, fixtures, indexed tests, xunit XML output and a command line filter, published under the Unlicense. It is a good fit for a C or C++ project that wants a test harness without a build system dependency, and a poor fit for teams that need mocks, parameterised suites or a large ecosystem around their tests.
Who is it for?
Adopt utest.h when you maintain a C or C++ project that has no test framework yet, or that has outgrown a hand-rolled harness, and you want the whole thing to be one file you can drop into a build with no dependencies to resolve. Do not adopt it if your team needs death tests, mocking, custom matchers, or the wider tooling ecosystem that a larger framework brings, since none of those appear in the documented macro set.
Can I use it commercially?
Yes. Unlicense is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 17 days ago.
What is it written in?
Mainly C++, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The whole framework is one file you copy in

The usage instruction in the README is a single line: include `utest.h` in your code. That is the entire installation story, and it is the reason this project exists in the shape it does.

A C or C++ project usually has no package manager to add a test dependency to, no module system to import one from, and often a build that predates any of that. Adding a testing framework usually means vendoring a source tree, wiring a build system, and accepting a dependency tree. utest.h removes that cost: the repository contains the `utest.h` header at its root, a `test/` directory, a `.clang-format` configuration and a `.github/` directory, and that is the whole top level.

The library is written in C and C++ and the repository lists C++ as its primary language. The licence field on the repository says Unlicense, which is close to public domain dedication, so vendoring the header into a commercial codebase raises fewer questions than most licences would. That is a practical consideration for a header that is meant to be copied rather than linked.

For a project that has never had tests, or that has a bespoke assert macro and a main function written by hand, this is a genuinely small step up. The framework gives the familiar bracketed output, per-test grouping, fixtures and a way to produce a report a CI system can read.

The trade is that a single header is also a single point of failure. There is no package to update, no upstream to pull fixes from, and no guarantee that the version you vendored matches what the author publishes next. Vendoring means taking a copy, and taking a copy means owning the upgrade.

Three ways to declare a test: UTEST, UTEST_F and UTEST_I

The macro surface is organised around three ways of writing a test, and each one is a different answer to a different question about test data.

`UTEST(set, name)` is the plain form. The first parameter is the set the test belongs to and the second is the test name, and the grouping exists purely for convenience. The minimal example in the README is three lines:

c
#include "utest.h"

UTEST(foo, bar) {
  ASSERT_TRUE(1);
}

`UTEST_F` runs a test against a shared fixture struct, and the struct is handed to the test through an implicit pointer named `utest_fixture`. The fixture needs two companion macros, `UTEST_F_SETUP` and `UTEST_F_TEARDOWN`, and the README is explicit that every fixture must have both, even if the body does nothing. Setup and teardown can contain assertions themselves, which is a small but useful detail, since a fixture that cannot be prepared correctly can fail loudly at the point of setup rather than inside each test.

The fixture example in the README sets a char, an int and a float, then asserts on one of them:

c
struct MyTestFixture {
  char c;
  int i;
  float f;
};

UTEST_F_SETUP(MyTestFixture) {
  utest_fixture->c = 'a';
  utest_fixture->i = 42;
  utest_fixture->f = 3.14f;

  // we can even assert and expect in setup!
  ASSERT_EQ(42, utest_fixture->i);
  EXPECT_TRUE(true);
}

`UTEST_I` is the third form, for when the same test should run repeatedly with a varying input. The setup and teardown macros are prefixed `UTEST_I_SETUP` and `UTEST_I_TEARDOWN`, the test is declared with `UTEST_I`, and an extra implicit variable `utest_index` is available so the fixture can change per iteration. The third parameter of `UTEST_I` is the number of times to run, and the README insists it must be a literal, which is a direct consequence of how the macro expands into a loop at compile time.

That is a reasonable set of three. What is not there is the wider list of test types a larger framework offers, and the README does not mention parameterised value tables, death tests, or typed test suites.

ASSERT ends the test, EXPECT lets it run on

The assertion macros come in two variants for every condition, and the difference between them is the thing a test author needs to internalise.

When an `ASSERT` fails, the test case stops executing right there and the framework moves on to the next test. When an `EXPECT` fails, the rest of the test still runs, so the remaining checks are carried out and you see every problem in one pass rather than one per run. The README frames this as matching what googletest provides, and the two names follow the same convention.

The conditions themselves are ordinary comparisons. The README documents `ASSERT_TRUE(x)` for a non-zero value, `ASSERT_FALSE(x)` for zero, `ASSERT_EQ(x, y)`, `ASSERT_NE(x, y)`, `ASSERT_LT(x, y)` and `ASSERT_LE(x, y)`, and each of these has an `EXPECT_` counterpart. The documented list is cut off partway through, at `ASSERT_LE`, so the complete set of conditions has to be read from the header itself rather than from the README.

A detail worth noticing in the examples is the argument order the framework uses, with the expected value first in the comparison forms, as in `ASSERT_EQ(42, utest_fixture->i)` and `ASSERT_EQ(a, b + 1)` for the failing case. When a comparison fails, the framework prints the two operands, and the order determines which one is labelled as the expected value in the output. Getting that backwards makes failure messages harder to read, and it is a habit worth forming from the first test you write.

The `EXPECT_` macros are the ones to reach for by default in a test that checks several independent things, and the `ASSERT_` macros for the ones where continuing would crash, since a null dereference after a failed precondition is a much worse debugging experience than a clean stop.

Command line options for filtering, xunit XML and random order

The framework takes command line arguments, and the set is small enough to describe in full.

`--help` prints the help message. `--filter=<filter>` selects which test cases run, and the README gives the motivating example directly: re-running one particular offending test case. `--list-tests` prints the test names one per line, and those names are exactly what you pass to `--filter`, so the workflow of finding a name and filtering on it is closed.

`--output=<output>` writes an xunit XML file with the results, and the README names the consumers it is aimed at: Jenkins, travis-ci and appveyor. This is the option that turns the framework from a local convenience into something a continuous integration job can report on, and it is the one to check first if you are evaluating utest.h for a build pipeline.

`--enable-mixed-units` changes the per-test output so timings appear in mixed units of s, ms, us and ns, which matters when a test suite includes a mix of microsecond checks and a few slow cases and you want one readable line per test.

`--random-order[=<seed>]` randomises the order the tests run in, and the seed is optional. If you do not supply one, a random starting seed is used. That is a feature aimed at exposing tests that depend on each other, and supplying a seed is what makes a failure reproducible after the fact, so a team that turns this on in CI should record the seed.

The output format is deliberately close to Google's framework, as the README states, with a run line, a per-test line and a summary:

code
[==========] Running 1 test cases.
[ RUN      ] foo.bar
[       OK ] foo.bar (631ns)
[==========] 1 test cases ran.
[  PASSED  ] 1 tests.

Worth noticing: the timings are in the output per test, which makes utest.h a quick way to see which case in a suite is slow, even if you never use the XML report.

Compilers, and the one caveat that will bite a tcc user

The supported platforms are Linux, macOS and Windows. The supported compilers are gcc, clang, MSVC's `cl.exe` and `clang-cl.exe`.

Then there is tcc, and the README is unusually specific about it, which is a good sign that someone hit the problem. It works with tcc, with a caveat: the latest release of the tcc compiler, version 0.9.27, lacks a feature the framework needs. The fix is to use a tcc patched with the constructor attribute extension, and the README notes that recent Ubuntu and Debian distributions already ship tcc with that patch. If you compile tcc yourself, use the trunk version.

The missing feature is the constructor attribute, which tells you how the framework discovers test cases. Rather than requiring a registration call in every file, the framework relies on the compiler marking a function as a global constructor so the test list is built before main runs. That is a neat trick, since it means adding a test file needs no build system change, but it also means the framework inherits a dependency on a compiler extension, and the toolchain with the least complete support for extensions is the one that breaks.

This is the single most useful thing to check before you adopt the library. If your project builds with tcc on a distribution whose packages predate the patch, the framework will not work, and the fix is a toolchain change rather than a configuration change in your code.

The repository also carries a `.clang-format` configuration, so the header and its tests follow a specific formatting style, and the CI badge in the README points at a CMake workflow, which tells you the project's own build is driven by CMake.

First use: UTEST_MAIN, or your own main function

There are two ways to start the framework, and the choice depends on whether you control the entry point of your program.

In one C or C++ file you call the macro that does everything:

c
UTEST_MAIN()

That calls into utest.h, instantiates all the test cases and runs the framework.

If you want to keep your own main, for instance because the same binary is both the application and the test runner, you instead declare the state in one file:

c
UTEST_STATE();

and then call into the framework yourself when you are ready:

c
int main(int argc, const char *const argv[]) {
  // do your own thing
  return utest_main(argc, argv);
}

The `utest_main` function takes the argument count and the argument vector, which is why the command line options work in both modes: the framework receives `argc` and `argv` either way.

Putting this together, a first test in a new project is: copy `utest.h` into the source tree, add a file that includes it and defines a `UTEST` case, add `UTEST_MAIN()` in exactly one file, and compile the result as a second binary alongside the normal one. The README does not give a compile command, and it does not describe a CMake or Make integration for the test target, so how the test binary is built is left to your build system. Since the header needs no linking, that part is a matter of adding one source file to a second target.

What utest.h does not do, and the Unlicense

The honest way to evaluate a single header framework is against what it leaves out, and the gaps are consistent.

There is no mocking layer. The README documents fixtures, indexed tests and comparison macros, and nothing about substituting objects or intercepting calls. A codebase that needs fakes for external services will have to write them by hand as ordinary C.

There is no death test support, no custom matcher vocabulary, and no typed or value-parameterised test suites. The comparison macros take two values and compare them, which covers equality and ordering but not anything with a tolerance, a substring check or a container comparison. A test needing approximate float comparison will need its own helper.

There is no test discovery from a directory or a naming convention, no grouping of tests into suites beyond the set name, no per-test tags, and no way to skip a test other than commenting it out. The `--filter` option gives you selection by name, which covers the common case of running one test locally, but there is no marker-based skipping.

There is also no thread or async support documented, which matters for C++ projects with a concurrency layer, since a test that starts a thread that outlives the test case will not be waited on.

The licence is the Unlicense, which is a dedication to the public domain rather than a conventional permissive licence. For most codebases that means you can vendor the header, modify it, and ship it inside a closed source product without attribution or copyleft obligations. It also means there is no warranty and no obligation on the author side, so the header is yours to maintain once copied. The release history is thin, with a single release tag named `pre-ai` dated 2026-06-04, and the last push to the repository was on 2026-10-06. The repository is not archived, but the versioning gives you nothing to track upstream against, so treat the vendored copy as the version you run.

Editorial conclusion

Adopt utest.h when you maintain a C or C++ project that has no test framework yet, or that has outgrown a hand-rolled harness, and you want the whole thing to be one file you can drop into a build with no dependencies to resolve. Do not adopt it if your team needs death tests, mocking, custom matchers, or the wider tooling ecosystem that a larger framework brings, since none of those appear in the documented macro set. Verify three things first: that your compiler is one of gcc, clang, cl.exe or clang-cl.exe, or a patched tcc, that your CI can read the xunit XML produced by the output option, and that the assertion macros you rely on are present in the copy of the header you are about to vendor, because the documented list is truncated in the README.

Frequently asked questions

How do I add utest.h to a C project?

Copy the utest.h header into your project and include it, then call UTEST_MAIN() in one source file. The README states the supported platforms are Linux, macOS and Windows, and the supported compilers are gcc, clang, cl.exe and clang-cl.exe.

What is the difference between ASSERT and EXPECT in utest.h?

A failed ASSERT stops the current test case and the framework moves to the next one, while a failed EXPECT lets the rest of the test run so that further checks are still carried out. Every condition has both an ASSERT_ and an EXPECT_ variant.

Does utest.h work with tcc?

Yes, with a caveat. The README states that the latest tcc release, version 0.9.27, lacks a feature the framework needs, and that a tcc patched with the constructor attribute extension is required. Recent Ubuntu and Debian distributions already ship that patch.

Can utest.h produce output for Jenkins or Travis CI?

Yes. The output option writes an xunit XML file with the test results, and the README says Jenkins, travis-ci and appveyor can parse it. The list-tests and filter options let you select which cases run.

How do I use fixtures and indexed tests in utest.h?

A fixture uses the UTEST_F macro with the fixture struct name, and requires both UTEST_F_SETUP and UTEST_F_TEARDOWN even if the bodies are empty. An indexed test uses UTEST_I with a run count as the third parameter, and the setup and teardown can read the utest_index variable to vary the fixture.

Official sources

  1. Issues
  2. License: Unlicense
  3. README
  4. Releases
  5. sheredom/utest.h on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/sheredom-utest-h.svg)](https://hysenlabs.com/projects/sheredom-utest-h)

Was this useful?