Fuzz tests are instantiated with the FUZZ_TEST macro. Here's an example:
void CallingMyApiNeverCrashes(int x, const std::string& s) {
// Exercise MyApi() with different inputs trying to trigger a crash, e.g.,
// by invalidating assertions in the code or by causing undefined behaviour.
bool result = MyApi(x, s);
ASSERT_TRUE(result); // The test itself can have assertions too.
}
FUZZ_TEST(MyApiTestSuite, CallingMyApiNeverCrashes)
.WithDomains(/*x:*/fuzztest::InRange(0,10),
/*s:*/fuzztest::Arbitrary<std::string>())
.WithSeeds({{5, "Foo"}, {10, "Bar"}});
The three parts of the instantiation that we discuss in more detail are the following:
- The property function (called
CallingMyApiNeverCrashes
in the example). - The input domains for the property function's parameters, specified using
the
.WithDomains()
clause. - The initial seed values for the parameters, specified using the
.WithSeeds()
clause.
The property function is a mandatory part of the instantiation; the input domains and the seeds are optional. If you want to specify both the input domains and the seeds, you have to specify the input domains first.
WARNING: The FUZZ_TEST
macro expands into a global variable definition. Thus,
you should be careful when initializing the input domains or seeds with other
global objects. Never initialize the input domains or seeds with global objects
that are initialized in other compilation units! Doing this may lead to the
static initialization order fiasco.
You may use seed providers to delay the initialization of
seeds.
The most important part of the above example is the CallingMyApiNeverCrashes
function, which we call the "property function". Fuzz tests are parameterized
unit tests, also called property-based tests. The tested property is captured
by a function with some parameters. The test will run this function with many
different argument values. We call this function the "property function",
because the name typically represent the tested property, for example
CallingMyApiNeverCrashes
in the example above.
The FUZZ_TEST
macro makes CallingMyApiNeverCrashes
function an actual fuzz
test. Note that CallingMyApiNeverCrashes
is both the name of our property
function and the test as well. The first parameter of the macro is the name of
the test suite, and the second is the name of the test (similarly to
GoogleTest's
TEST()
macro). The test suite MyApiTestSuite
can contain multiple
FUZZ_TEST
-s and regular unit TEST
-s as well. The property function can have
one or more of parameters.
Important things to note about the property function:
- The function will be run with many different inputs in the same process, trying to trigger a crash (assertion failure).
- The return value should be
void
, as we only care about invalidating assertions and triggering undefined behavior. - The function should not
exit()
on any input, because that will terminate the test execution. - Ideally, it should be deterministic (same input arguments should trigger the same execution). Non-determinism can make fuzzing inefficient.
- Ideally, it should not use any global state (only depend on input parameters).
- It may use threads, but threads should be joined in the function.
- It's better to write more smaller functions (and
FUZZ_TEST
s) than one big one. The simpler the function, the easier it is for the tool to cover it and find bugs.
As opposed to
value-parameterized tests,
the parameters in property-based tests are not assigned to a set of concrete
values, but to an abstract input domain. The input domain of each parameter of
the property function can be assigned using the .WithDomains()
clause. An
input domain is an abstraction representing a typed set of values. In the
example above, we specify that the input domain of the first parameter is "any
integer between 0 and 10" (closed interval), while the input domain of the
second parameter is "an arbitrary string". If we don't explicitly specify
domains, for a parameter of type T
, the fuzztest::Arbitrary<T>()
domain will
be used.
Input domains are the central concept in FuzzTest. The input domain
specifies the coverage of the property-based testing inputs. The most commonly
used domain is Arbitrary<T>()
, which represent all possible values of type
T
. This is also the "default" input domain. This means that when we want use
Arbitrary<T>()
for every parameter of our property function, for example:
FUZZ_TEST(MyApiTestSuite, CallingMyApiNeverCrashes)
.WithDomains(/*x:*/fuzztest::Arbitrary<int>(),
/*s:*/fuzztest::Arbitrary<std::string>());
then the input domain assignment using .WithDomains()
can be omitted
completely:
FUZZ_TEST(MyApiTestSuite, CallingMyApiNeverCrashes);
See the Domains Reference for the descriptions of the various built-in domains and also how you can build your own domains.
You can use the .WithSeeds()
clause to provide the initial seed values for the
property function's parameters. The initial seed values are concrete inputs that
FuzzTest deterministically runs and uses as a basis for creating other inputs.
In the above example, the seed {5, "Foo"}
causes FuzzTest to execute
CallingMyApiNeverCrashes(5, "Foo")
. In the subsequent runs, FuzzTest may
generate similar values, like {5, "Foooo"}
or {10, "Foo"}
, and use them as
inputs to the property function.
Providing seeds is not necessary; if you don't provide them, FuzzTest will generate them at random. However, in certain cases providing seeds can greatly improve fuzzing efficiency.
Note: Some domains don't support seeds. ElementOf
and Just
support seeds
only for certain types (see ElementOf
).
Complex domains constructed using combinators ConstructorOf
, Map
, and
FlatMap
don't support seeds.
As noted earlier, fuzz tests are instantiated as global variables, so
initializing seeds from other global objects (especially those coming from other
compilation units) can lead to unexpected consequences or even undefined
behavior. For cases when such initialization cannot be avoided, the
.WithSeeds()
clause also accepts a seed provider—a function that returns a
vector of seeds. FuzzTest will call the seed provider in runtime, thus avoiding
any static initialization issues.
The seed provider can be any invocable (lambda, function pointer, callable
object, etc.) that doesn't take inputs and returns
std::vector<std::tuple<Args...>>
, where Args...
are the types of the
property function's parameters. In the example from earlier, we could use the
following seed provider:
FUZZ_TEST(MyApiTestSuite, CallingMyApiNeverCrashes)
.WithSeeds([]() -> std::vector<std::tuple<int, std::string>> {
return {{5, "Foo"}, {10, "Bar"}};
});
If you are using a test fixture, the seed provider can also be a pointer to a member function of the fixture, as in the following example:
class MyFuzzTest {
public:
void CallingMyApiNeverCrashes(int x, const std::string& s);
// The seed provider can depend on the fixture's state.
std::vector<std::tuple<int, std::string>> seeds() { return seeds_; }
private:
std::vector<std::tuple<int, std::string>> seeds_ = {{5, "Foo"}, {10, "Bar"}};
};
FUZZ_TEST_F(MyFuzzTest, CallingMyApiNeverCrashes)
// The seed provider can be a pointer to the fixture's member function. Note
// that the member function can't be const.
.WithSeeds(&MyFuzzTest::seeds);
You can also provide a checked-in corpus of seeds using
fuzztest::ReadFilesFromDirectory
, which supports loading
strings. For example:
static constexpr absl::string_view kMyCorpusPath = "path/to/my_corpus";
void CallingMyApiNeverCrashes(const std::string& input) {
...
}
FUZZ_TEST(MyApiTestSuite, CallingMyApiNeverCrashes)
.WithSeeds(fuzztest::ReadFilesFromDirectory(
absl::StrCat(std::getenv("TEST_SRCDIR"), kMyCorpusPath)));