blob: 7e6b886d5f66ff78edd4b75aa71c498f6952aa54 [file] [log] [blame] [view]
# Test Fixtures
If your fuzz test requires setup/teardown logic that is too expensive to be
embedded in your [property function](fuzz-test-macro.md#the-property-function)
and executed on each fuzz test iteration, you can use a
test fixture.
In FuzzTest, any default-constructible class can serve as a test fixture.
To instantiate it, use the macro
FUZZ_TEST_F,
which corresponds to [`FUZZ_TEST`](fuzz-test-macro.md) in the same way that
`TEST_F` corresponds to `TEST` in GoogleTest. Furthermore, you can easily adapt
and reuse the existing
[GoogleTest fixtures](https://google.github.io/googletest/primer.html#same-data-multiple-tests)
in your fuzz tests.
[TOC]
## When to use fixtures in fuzz tests
Most best practices for writing unit tests
(https://abseil.io/tips/122)
apply equally well when writing fuzz tests. In view of that, you should
generally avoid fixtures and prefer standalone helper functions for your fuzz
test's initialization. However, in some situations test fixtures are
appropriate.
1. If your fuzz test requires expensive setup, e.g., initializing a server,
doing it in the property function may be prohibitively expensive. The
fuzzing engine calls the property function once per fuzz test iteration, and
running the setup each time may slow down fuzzing to a point where it
becomes ineffective.
2. If you want to add a fuzz test to an existing code base that uses test
fixtures for unit tests, refactoring the code to eliminate fixtures may be
impractical or even impossible.
## Using a fixture for expensive setup
Suppose we wanted to test that a certain HTTP server called EchoServer echoes
back the string it receives. We could do this with a fuzz test that doesn't use
fixtures.
```c++
// A standalone property function that tests the property of interest.
void ReturnsTheSameString(const std::string& request) {
// Initialize the server.
EchoServer server;
server.Start("localhost:9999");
// Test the property.
std::string response;
SendRequest("localhost:9999", request, &response);
EXPECT_EQ(response, request);
// Tear down the server.
server.Stop();
}
// Instantiate the fuzz test using the FUZZ_TEST macro with a chosen suite name
// and the property function as the test name.
FUZZ_TEST(EchoServerFuzzTest, ReturnsTheSameString)
.WithDomains(fuzztest::Arbitrary<std::string>())
.WithSeeds({"Hello"});
```
The issue with this fuzz test is that the server initialization and teardown
happen in each fuzz test iteration, that is, in each call to the property
function. If starting and stopping the server takes non-trivial time, the fuzz
test will be slow and ineffective.
To improve the test, we would like to do the initialization and teardown once
and reuse the same server across all fuzz test iterations. We can do this using
a test fixture:
```c++
// In FuzzTest, any default-constructible class can be a test fixture.
class EchoServerFuzzTest {
public:
// The constructor initializes the fixture.
EchoServerFuzzTest() { server_.Start("localhost:9999"); }
// The destructor tears down the fixture.
~EchoServerFuzzTest() { server_.Stop(); }
// The fuzz test's property function must be a public member of the fixture.
void ReturnsTheSameString(const std::string& request) {
std::string response;
SendRequest("localhost:9999", request, &response);
EXPECT_EQ(response, request);
}
private:
EchoServer server_;
};
// Instantiate the fuzz test using the FUZZ_TEST_F macro, with the fixture as
// the suite name and the property function as the test name.
FUZZ_TEST_F(EchoServerFuzzTest, ReturnsTheSameString)
// The macro supports the FUZZ_TEST clauses for specifying domains and seeds.
.WithDomains(fuzztest::Arbitrary<std::string>())
.WithSeeds({"Hello"});
```
FuzzTest will instantiate the fixture once and call the property function
on the same object in each fuzz test iteration.
### Note on mutable test fixtures
While persisting state across fuzz test iterations can be effective, it can also
have unwanted consequences. Consider the following example.
```c++
class MyFuzzTest {
public:
void MyProperty(int n) {
MyApi(n, flag_);
flag_ = !flag_;
}
private:
bool flag_ = false;
};
FUZZ_TEST_F(MyFuzzTest, MyProperty);
```
By mutating `flag_` and using it in the call to `MyApi()`, the test no longer
depends only on the input generated by the fuzzer. This can potentially break
the fuzzer's assumptions about how the inputs affect coverage and make fuzzing
ineffective. Additionally, a crashing input found by the fuzzer may not be
sufficient for reproducing the crash, since the crash also depends on a
particular value of `flag_`.
BEST PRACTICE: Make sure that the property function resets the mutated fixture
so that each fuzz test iteration starts in the same state.
## Using existing GoogleTest fixtures in fuzz tests
Note: Only use GoogleTest fixtures if they are shared with existing unit tests,
or if you need static per-fixture setup and teardown (via `SetUpTestSuite` and
`TearDownTestSuite`). Otherwise, prefer fixtureless code with
[standalone initialization functions](https://abseil.io/tips/122).
If you already use a GoogleTest fixture derived from `::testing::Test` in your
unit tests and you want to reuse it in fuzz tests, you first need to choose when
you want the fixture to be instantiated and destroyed. GoogleTest fixtures are
typically built around a guarantee that the same fixture object will never be
reused for multiple tests or multiple runs of the same test. In a fuzz test,
this guarantee requires instantiating the fixture *once per fuzz test
iteration*, i.e., calling the property function each time on a fresh fixture
object. However, for increased performance, it is possible to instantiate a
fixture *once per fuzz test* (if the fixture supports such persistence) and
reuse the same fixture object in all calls to the property function. As noted
earlier, this comes with the risk of state mutation, which can lead to
non-reproducible crashes, so design your fixture carefully.
### Semantics 1: Fixture instantiated once per fuzz test iteration
Adapt a fixture called `Fixture` by extending
`::fuzztest::PerIterationFixtureAdapter<Fixture>`. This case should be the more
common one, since it observes the GoogleTest invariant of not using a fixture
object more than once.
As an example, suppose we have a function `SumVec()` that returns the sum of a
vector of integers and we have the following fixture.
```c++
class SumVecTest : public testing::Test {
public:
SumVecTest() : vec_{1, 2, 3} {}
protected:
std::vector<int> vec_;
};
```
A fuzz test that uses the fixture would look like the following:
```c++
// Adapts the fixture using the "per-iteration" semantics.
class SumVecFuzzTest : public fuzztest::PerIterationFixtureAdapter<SumVecTest> {
public:
void SumsLastEntry(int last_entry) {
int previous_sum = SumVec(vec_);
vec_.push_back(last_entry);
EXPECT_EQ(SumVec(vec_), previous_sum + last_entry);
}
};
FUZZ_TEST_F(SumVecFuzzTest, SumsLastEntry);
```
The "per-iteration" semantics is appropriate for this fixture because the test
mutates `vec_`.
### Semantics 2: Fixture instantiated once per fuzz test
Adapt a fixture called `Fixture` by extending
`::fuzztest::PerFuzzTestFixtureAdapter<Fixture>`. This case should be the less
common one: it relies on the fixture being easily resettable, which is not a
typical concern when designing GoogleTest fixtures.
As an example, let's return to EchoServer. Suppose we already have a GoogleTest
fixture that sets up the server.
```c++
class EchoServerTest : public testing::Test {
public:
EchoServerTest() { server_.Start("localhost:9999"); }
~EchoServerTest() override { server_.Stop(); }
private:
EchoServer server_;
};
```
We would write a test using this fixture as in the following code:
```c++
// Adapts the fixture using the "per-fuzz-test" semantics.
class EchoServerFuzzTest
: public fuzztest::PerFuzzTestFixtureAdapter<EchoServerTest> {
public:
void ReturnsTheSameString(const std::string& request) {
std::string response;
SendRequest("localhost:9999", request, &response);
EXPECT_EQ(response, request);
}
};
FUZZ_TEST_F(EchoServerFuzzTest, ReturnsTheSameString);
```
Here, the "per-fuzz-test" semantics is appropriate because the test doesn't
mutate the server in any way that would be relevant for the test, and the server
initialization is too expensive to be performed in each iteration.
## Advanced fixtures with runner functions
When using non-GoogleTest fixtures, FuzzTest can make use of *runner functions*
on the fixture to specify per-test or per-iteration setups in a more flexible
way. FuzzTest provides the following runner interfaces for a fixture to inherit:
- `::fuzztest::FuzzTestRunnerFixture`, which declares the runner function
`void FuzzTestRunner(absl::AnyInvocable<void() &&> run_test)`
If the interface is inherited, instead of directly running the fuzz test,
FuzzTest will call `FuzzTestRunner`, expecting that it will in turn:
1. Set up everything needed for running the test.
2. Invoke `run_test`, through which FuzzTest will essentially pass the
whole fuzzing loop with multiple invocations to the property function.
The function `run_test` will return once FuzzTest is done with the test.
3. Tear down everything no longer needed after the test.
- `::fuzztest::IterationRunnerFixture`, which declares the runner function
`void FuzzTestIterationRunner(absl::AnyInvocable<void() &&> run_iteration)`
If the interface is inherited, instead of directly running a fuzz test
iteration, FuzzTest will call `FuzzTestIterationRunner`, expecting that it
will in turn:
1. Set up everything needed for running a test iteration.
2. Invoke `run_iteration`, through which FuzzTest will invoke the property
function once. The function `run_iteration` will return once the
iteration is done.
3. Tear down everything no longer needed after the test iteration.
Note that the type `absl::AnyInvocable<void() &&>` ensures that the functions
`run_test` and `run_iteration` can be called only once. To call them, you must
first move them, for example `std::move(run_test)()`. Attempting to call these
functions more than once will result in a runtime error.
The traditional SetUp/TearDown functions of a fixture can be trivially
transformed into a runner function. For example, the `EchoServerFuzzTest`
fixture could be defined as:
```c++
class EchoServerFuzzTest : public fuzztest::FuzzTestRunnerFixture {
public:
void FuzzTestRunner(absl::AnyInvocable<void() &&> run_test) override {
server_.Start("localhost:9999");
std::move(run_test)();
server_.Stop();
}
// The fuzz test's property function
void ReturnsTheSameString(const std::string& request) {
std::string response;
SendRequest("localhost:9999", request, &response);
EXPECT_EQ(response, request);
}
private:
EchoServer server_;
};
FUZZ_TEST_F(EchoServerFuzzTest, ReturnsTheSameString);
```
To create a new `EchoServer` instance for each fuzz test iteration, define
`FuzzTestIterationRunner` instead:
```c++
class EchoServerFuzzTest : public fuzztest::IterationRunnerFixture {
public:
void FuzzTestIterationRunner(absl::AnyInvocable<void() &&> run_iteration) override {
server_.Start("localhost:9999");
std::move(run_iteration)();
server_.Stop();
}
...
```
Although converting SetUp/TearDown to runner functions is trivial, the other way
around may not even be possible. For example, suppose there is a fuzz test whose
property function must run within an SUT created by an SUT method
`RunWithinSut(sut_ready_cb)`, which starts the SUT and calls the callback when
the SUT is ready, and shuts down the SUT when the callback returns. Suppose
additionally that `RunWithinSut` must be run in the main thread of the test,
which is also the thread where FuzzTest runs the test fixture. Given these
constraints, it is impossible to run such test with a SetUp/TearDown-style
fixture without calling `RunWithinSut` for every iteration, which may be too
heavy and impact the fuzzing performance. With runner functions, the fixture can
be defined as:
```c++
class SutBasedFuzzTest : public fuzztest::FuzzTestRunnerFixture {
public:
void FuzzTestRunner(absl::AnyInvocable<void() &&> run_test) {
sut_.RunWithinSut([&] { std::move(run_test)(); });`.
}
private:
SUT sut_;
}
```