CinyTest
CinyTest is a lightweight library for defining and running unit tests in C. It targets the C17 standard (mostly) and follows the guidelines of Modern C.
Features
- unit test and suite creation
- unit suite execution, including setup and teardown support
- configurable test output to stdout
- include and exclude test filtering
- JUnit-style output support
- test assertions including:
- value equality, including approximate equality for floating point
- pointer equality and NULL testing
- C-string equality
- general true/false asserts
Build CinyTest
The Makefile will build CinyTest static and dynamic (shared) libraries for either macOS (clang) or Linux (gcc). The mac folder also contains an Xcode project used for development that only builds a static library.
For Windows builds there is a batch file: winbuild.bat. Microsoft's dev tools do not support several C99 and C11 features so the batch file uses clang instead of cl.exe. See Windows Support below for more detail.
The Makefile has the following build targets:
release
the default target; build CinyTest libraries and binary tree sample executables with full optimizationsdebug
build CinyTest libraries and binary tree sample executables with debug symbols and no optimizationscheck
verify CinyTest by running the sample testsinstall
add CinyTest artifacts to /usr/localuninstall
remove CinyTest artifacts from /usr/localclean
delete all build artifacts; run this when switching between debug and release builds to make sure all artifacts are rebuilt
All of the Makefile artifacts are placed in a folder named build.
The Windows batch file emulates a simplified version of the Makefile, building all targets unconditionally and clearing any previous build artifacts each time.
The Xcode project consists of the following targets:
- CinyTest: main target for CinyTest source code and unit tests; builds the CinyTest static library and runs the XCTest unit tests
- Sample: additional target illustrating the use of CinyTest to test a binary tree module; includes command-line binary to exercise the binary tree module and an XCTest class for bootstrapping the binary tree unit tests into Xcode
Finally, documentation can be generated using the Doxygen configuration file provided in the doc folder.
Example
A simple example demonstrating the use of CinyTest as an external dependency can be found in the CinyTest-Example repo.
Running this example on the command line (assuming all tests pass) will output something like the following:
Test suite 'rectangle_tests' started at 2019-09-03T22:24:06-0700
[ ✓ ] - 'rectanglemake_createsrectangle' success
[ ✓ ] - 'rectanglearea_calculatesarea' success
[ ✓ ] - 'rectanglehypotenuse_calculateshypotenuse' success
[ ✓ ] - 'rectangletostring_buildsrectanglestring' success
[ ✓ ] - 'rectangleprint_printsrectangle' success
Ran 5 tests (0.000 seconds): 5 passed.
Test suite 'circle_tests' started at 2019-09-03T22:24:06-0700
[ ✓ ] - 'circlemake_createscircle' success
[ ✓ ] - 'circlediameter_calculatesdiameter' success
[ ✓ ] - 'circlearea_calculatesarea' success
[ ✓ ] - 'circletostring_buildscirclestring' success
[ ✓ ] - 'circleprint_printscircle' success
Ran 5 tests (0.001 seconds): 5 passed.
[ SUCCESS ] - Ran 10 tests (0.001 seconds): 10 passed.
This example runs two test suites of four test cases each.
Options
Any program that CinyTest is compiled into will support a set of command-line options and environment variables that control CinyTest behavior. Command-line options take precedence over environment variables if both are specified.
CLI Option | Environment Variable | Description |
---|---|---|
--ct-help |
N/A | Print CinyTest usage |
--ct-version |
N/A | Print CinyTest version |
--ct-verbose |
CINYTEST_VERBOSE |
Set output verbosity |
--ct-colorized |
CINYTEST_COLORIZED |
Toggle colorized output in test results |
--ct-suppress-output |
CINYTEST_SUPPRESS_OUTPUT |
Toggle capture of standard stream output |
--ct-include |
CINYTEST_INCLUDE |
Include test filters; see Test Filters section below for more details |
--ct-exclude |
CINYTEST_EXCLUDE |
Exclude test filters; see Test Filters section below for more details |
--ct-xml |
CINYTEST_XML |
Write a JUnit XML report to the specified file |
Test Filters
Test filters are used to select a subset of all the tests in a test run. Include filters run matched tests and skip everything else. Exclude filters skip matched tests and run everything else. Include filters are checked before exclude filters, so if both options are specified then tests that match any include filters and do not match any exclude filters will run.
Test filters can target individual test cases or an entire test suite using a simple format. A filter expression consists of a suite-pattern followed by a case-pattern separated by a ':'
. Either the suite-pattern or the case-pattern are optional and specifying one, the other, or both will result in different matching behavior. Both patterns can use '?'
to match any single character and '*'
to match any string of characters. Multiple filter expressions of the same type are separated by ','
.
Examples:
./mytests
no filters, all tests are run; equivalent to--ct-include='*'
./mytests --ct-include='foo_tests:frob_returns_true'
run only thefrob_returns_true
test in thefoo_tests
suite./mytests --ct-include='foo_tests:'
run all tests infoo_tests
suite; this is shorthand for--ct-include='foo_tests:*'
./mytests --ct-include=':verify_returns_null'
run any test namedverify_returns_null
across all suites; this is shorthand for--ct-include='*:verify_returns_null'
./mytests --ct-include='*foo'
run any case or suite that ends withfoo
./mytests --ct-include='bar?:'
run any suite starting withbar
followed by one character, e.g.barn
orbark
./mytests --ct-include='*foo*,*bar*'
run all tests withfoo
orbar
somewhere in the suite or case name./mytests --ct-exclude='bar_tests:'
run all tests except the cases in thebar_tests
suite./mytests --ct-include='*foo*' --ct-exclude='*bar*'
run all tests withfoo
in the name except those tests that also havebar
in the name (the order of the options does not matter; include is always checked before exclude)
A note of caution regarding wildcards: filters treat suite and case names as UTF-8 byte strings so multi-byte characters (such as high Basic Multilingual Plane characters or emojis) may be seen as more than one "character". This means the single character wildcard '?'
will never match a multi-byte character. Instead use the substring wildcard '*'
, as it will properly match the sequence of bytes that comprise the multi-byte character.
It is possible you may find your filters applying in unexpected ways, particularly when combining multiple filters in a single test run. Test filters can be debugged by turning up the output to its highest verbosity (--ct-verbose=3
). At this output level all tests are printed, including the skipped tests, and annotated with their matched filter (or no match
). In addition, all filter expressions are listed along with the rest of CinyTest's options at the start of the output.
Why CinyTest?
The idea for CinyTest arose out of an exploration of the current landscape of C unit testing frameworks. I was looking for a test library that specifically targeted C instead of C++ and did not require standalone executables or linker tricks. I wanted a library that could be used within an IDE for red-green-refactor test-driven development.
Many C test frameworks did not fit the bill; some required make files as part of the execution cycle, others needed per-suite linker flags or multiple main
function definitions. Some frameworks I found could be run in an IDE but were either heavier than I wanted or had what I felt were awkward or dated APIs. I decided to write my own and CinyTest is the result.
cmocka provided initial inspiration for CinyTest. While the design goals of the two libraries are different, cmocka's API was a guide for the generality and brevity I wanted to achieve with CinyTest.
CinyTest provides a straightforward and terse public API for writing unit tests in C. While CinyTest does not integrate directly into an IDE test framework (due to lack of tool support) it is trivial to include bare-bones driver code to hook into any test framework of choice while still maintaining all the actual test code in native C.
In the sample code included in the CinyTest project, CinyTest is bootstrapped into XCTest using a small Objective-C test class. A similar approach would work for any other programming environment that can interoperate with C. In addition CinyTest could be run as a native C executable by defining and calling any test suites within a main
function, which could then be built and run using whatever tool fits your needs. This approach is shown in the CinyTest-Example repo and as the check
build target in the Makefile.
Constraints and Assumptions
While I hope CinyTest is useful to others it is also a hobby exercise of mine. Its criteria fit my specific use case and I approached the problem in a very deliberate way. I wanted to see if I could write a library that used no platform-specific compiler features and targeted the latest C language standard, C17.
That original goal has been left behind at this point as I've expanded the library's scope and target platforms. However the majority of platform-specific code is nicely sequestered behind a common API and compiler targets. As of this writing there are POSIX and Windows compatibility source files.
Memory Usage
CinyTest is designed to be a small and simple library. To that point, it uses no dynamic memory in any of its default behaviors. However there are some advanced features that do require memory allocations. If your environment is sensitive to unexpected memory usage then take care when invoking the following options:
- include and exclude test filters
- JUnit test report
Windows Support
CinyTest uses C99 and C11 features that are, as of this writing, not supported by Microsoft development tools natively; cl.exe is incapable of compiling CinyTest. Fortunately Microsoft has put some effort into supporting newer C standards and current versions of Visual Studio include clang tooling and runtime libraries that cover the later standards.
It is possible to develop CinyTest in Visual Studio by installing the C++ Clang tools for Windows feature (packaged in Visual Studio 2019+) but winbuild.bat is sufficient to build artifacts. Note that winbuild.bat only builds a static version of the CinyTest library.
To build CinyTest on Windows install the following tools:
- Visual Studio Community 2019
- Desktop development with C++
- clang
- Doxygen (set the
_WIN64
preprocessor macro in the Predefined section to select the right documentation)
Use the x64 Native Build Tools Command Prompt in order to get the correct environment.
A Word on Warnings
CinyTest strives for zero warnings under -Wall -Wextra -pedantic
. This is achieved for the library itself but certain usages of the library contain some minor caveats.
CinyTest uses variadic macros for its test assertions. The call signature has an optional assert message and can be called either as ct_asserttrue(foo)
or ct_asserttrue(foo, "Expected true expression")
.
However the ct_asserttrue(foo)
form will trigger a missing variadic arguments warning for -pedantic
. This can be suppressed either by always specifying an assert message (as shown above) or by including a trailing comma which satisfies the preprocessor: ct_asserttrue(foo,)
. In addition any tests that don't use the test context parameter will trigger unused parameter warnings if -Wextra
is used. This can be suppressed by casting the argument to void: (void)context;
.
I tend to be more lax for test code than production code so I would likely omit the warnings for unit tests. In my experience all warnings triggered by usage of CinyTest would be caught by -Wno-unused-parameter -Wno-gnu-zero-variadic-macro-arguments
(gcc does not define the latter flag so use -Wno-pedantic
on test builds). If that's an unappealing option then use one of the syntax remedies described above.
The Xcode project builds with the same warning level as the Makefile. However XCTest uses some GNU extensions and a couple macros that trigger -pedantic
issues so the project selectively scales back certain warnings either on the individual targets or as compiler pragmas when necessary. On Windows the UCRT headers mark several stdlib functions as deprecated that are supported fine on Linux and macOS so the batch file includes the -Wno-deprecated-declarations
option.
How do I pronounce CinyTest?
I don't know. The name is a visual pun on "C" and "Tiny". Some possibilities:
- TinyTest
- ChinyTest
- SinyTest
- ShinyTest
- ???