In our current testing environment, we spend a significant amount of effort crafting end-to-end tests for error conditions that could easily be captured by unit tests (or we simply forgo some hard-to-setup and rare error conditions). Unit tests additionally provide stability to the codebase and can simplify debugging through isolation. Writing unit tests in pure C, rather than with our current shell/test-tool helper setup, simplifies test setup, simplifies passing data around (no shell-isms required), and reduces testing runtime by not spawning a separate process for every test invocation.
We believe that a large body of unit tests, living alongside the existing test suite, will improve code quality for the Git project.
Definitions
For the purposes of this document, we’ll use test framework to refer to projects that support writing test cases and running tests within the context of a single executable. Test harness will refer to projects that manage running multiple executables (each of which may contain multiple test cases) and aggregating their results.
In reality, these terms are not strictly defined, and many of the projects discussed below contain features from both categories.
For now, we will evaluate projects solely on their framework features. Since we are relying on having TAP output (see below), we can assume that any framework can be made to work with a harness that we can choose later.
Summary
We believe the best way forward is to implement a custom TAP framework for the Git project. We use a version of the framework originally proposed in 1.
See the Framework Selection section below for the rationale behind this decision.
Choosing a test harness
During upstream discussion, it was occasionally noted that prove
provides many
convenient features, such as scheduling slower tests first, or re-running
previously failed tests.
While we already support the use of prove
as a test harness for the shell
tests, it is not strictly required. The t/Makefile allows running shell tests
directly (though with interleaved output if parallelism is enabled). Git
developers who wish to use prove
as a more advanced harness can do so by
setting DEFAULT_TEST_TARGET=prove in their config.mak.
We will follow a similar approach for unit tests: by default the test
executables will be run directly from the t/Makefile, but prove
can be
configured with DEFAULT_UNIT_TEST_TARGET=prove.
Framework selection
There are a variety of features we can use to rank the candidate frameworks, and those features have different priorities:
-
Critical features: we probably won’t consider a framework without these
-
Can we legally / easily use the project?
-
Does the project support our bare-minimum needs?
-
-
Nice-to-have features:
-
Tie-breaker stats
License
We must be able to legally use the framework in connection with Git. As Git is licensed only under GPLv2, we must eliminate any LGPLv3, GPLv3, or Apache 2.0 projects.
Vendorable or ubiquitous
We want to avoid forcing Git developers to install new tools just to run unit tests. Any prospective frameworks and harnesses must either be vendorable (meaning, we can copy their source directly into Git’s repository), or so ubiquitous that it is reasonable to expect that most developers will have the tools installed already.
Maintainable / extensible
It is unlikely that any pre-existing project perfectly fits our needs, so any project we select will need to be actively maintained and open to accepting changes. Alternatively, assuming we are vendoring the source into our repo, it must be simple enough that Git developers can feel comfortable making changes as needed to our version.
In the comparison table below, "True" means that the framework seems to have active developers, that it is simple enough that Git developers can make changes to it, and that the project seems open to accepting external contributions (or that it is vendorable). "Partial" means that at least one of the above conditions holds.
Major platform support
At a bare minimum, unit-testing must work on Linux, MacOS, and Windows.
In the comparison table below, "True" means that it works on all three major platforms with no issues. "Partial" means that there may be annoyances on one or more platforms, but it is still usable in principle.
TAP support
The Test Anything Protocol is a text-based interface that allows tests to communicate with a test harness. It is already used by Git’s integration test suite. Supporting TAP output is a mandatory feature for any prospective test framework.
In the comparison table below, "True" means this is natively supported. "Partial" means TAP output must be generated by post-processing the native output.
Frameworks that do not have at least Partial support will not be evaluated further.
Diagnostic output
When a test case fails, the framework must generate enough diagnostic output to help developers find the appropriate test case in source code in order to debug the failure.
Runtime-skippable tests
Test authors may wish to skip certain test cases based on runtime circumstances, so the framework should support this.
Parallel execution
Ideally, we will build up a significant collection of unit test cases, most likely split across multiple executables. It will be necessary to run these tests in parallel to enable fast develop-test-debug cycles.
In the comparison table below, "True" means that individual test cases within a single test executable can be run in parallel. We assume that executable-level parallelism can be handled by the test harness.
Mock support
Unit test authors may wish to test code that interacts with objects that may be inconvenient to handle in a test (e.g. interacting with a network service). Mocking allows test authors to provide a fake implementation of these objects for more convenient tests.
Signal & error handling
The test framework should fail gracefully when test cases are themselves buggy or when they are interrupted by signals during runtime.
Project KLOC
The size of the project, in thousands of lines of code as measured by sloccount (rounded up to the next multiple of 1,000). As a tie-breaker, we probably prefer a project with fewer LOC.
Adoption
As a tie-breaker, we prefer a more widely-used project. We use the number of GitHub / GitLab stars to estimate this.
Comparison
Framework | License | Vendorable or ubiquitous | Maintainable / extensible | Major platform support | TAP support | Diagnostic output | Runtime- skippable tests | Parallel execution | Mock support | Signal & error handling | Project KLOC | Adoption |
---|---|---|---|---|---|---|---|---|---|---|---|---|
[lime-background]#GPL v2# |
[lime-background]#True# |
[lime-background]#True# |
[lime-background]#True# |
[lime-background]#True# |
[lime-background]#True# |
[lime-background]#True# |
[red-background]#False# |
[red-background]#False# |
[red-background]#False# |
1 |
0 |
|
[lime-background]#ISC# |
[lime-background]#True# |
[yellow-background]#Partial# |
[lime-background]#True# |
[yellow-background]#Partial# |
[lime-background]#True# |
[lime-background]#True# |
[red-background]#False# |
[red-background]#False# |
[red-background]#False# |
3 |
1400 |
|
[lime-background]#MIT# |
[red-background]#False# |
[yellow-background]#Partial# |
[lime-background]#True# |
[lime-background]#True# |
[lime-background]#True# |
[lime-background]#True# |
[lime-background]#True# |
[red-background]#False# |
[lime-background]#True# |
19 |
1800 |
|
[lime-background]#Expat# |
[lime-background]#True# |
[yellow-background]#Partial# |
[yellow-background]#Partial# |
[lime-background]#True# |
[red-background]#False# |
[lime-background]#True# |
[red-background]#False# |
[red-background]#False# |
[red-background]#False# |
4 |
33 |
|
[lime-background]#LGPL v2.1# |
[red-background]#False# |
[yellow-background]#Partial# |
[lime-background]#True# |
[lime-background]#True# |
[lime-background]#True# |
[red-background]#False# |
[red-background]#False# |
[red-background]#False# |
[lime-background]#True# |
17 |
973 |
Milestones
-
Add useful tests of library-like code
-
Integrate with stdlib work
-
Run alongside regular
make test
target