Enhancing Code Quality with Fuzz Testing in Go
Written on
Chapter 1: Introduction to Fuzz Testing
Fuzz testing is an innovative approach that automatically generates input values for functions in order to uncover bugs that may not be immediately apparent.
Fuzzing was incorporated into the Go standard library starting from version 1.18. This feature is a valuable addition for detecting elusive bugs in your code. Many developers have discovered bugs in the Go standard library itself by utilizing third-party fuzzing tools. Since fuzzing is a form of testing, it integrates seamlessly with existing testing utilities.
In this article, we will explore how to utilize the new fuzzing capabilities by testing an HTTP handler we’ve created. Before you begin, ensure you have at least Go 1.18 installed. For installation guidance, refer to my overview of the changes introduced in version 1.18.
Chapter 2: Setting Up Fuzz Testing
To initiate fuzz testing, I established a new project and initialized a Go module. This requires a test file, which I named main_test.go. Here’s how you can set it up:
mkdir fuzzy
go mod init programmingpercy.tech/fuzzy
touch main_test.go
Before we can start fuzzing, we need a function to test. We’ll create a handler that determines the maximum value from a slice of integers. To demonstrate fuzzing, we'll intentionally introduce a bug into the handler: if the result is equal to 50, it will fail.
Understanding Fuzzing Mechanics
Now, let’s dive into using the fuzzer. If you're already familiar with Go testing, you’ll find this straightforward. A Go test is typically defined by a function that begins with "Test" and accepts a parameter of type *testing.T. Fuzz tests follow a similar convention but are prefixed with "Fuzz" and take a *testing.F parameter.
To kick things off, we need to supply the testing.F with a Seed Corpus, which should consist of example data reflecting the expected input to your function. The fuzzer will utilize this data to create new inputs.
You can add seeds using f.Add(), which can accept various data types, including:
- string
- []byte
- int
- float64
- bool
Keep in mind that you cannot mix data types. If you attempt to add a string followed by an integer, it will result in an error.
Handling Multiple Input Parameters
If your function requires multiple parameters, f.Add() can handle a variable number of inputs. Just ensure that the order of your example data matches the parameter order of the function being tested.
Now, let’s create example data for our handler and add it. Since we’re fuzzing an HTTP handler, we will use JSON data encoded as []byte.
We will set up an HTTP server that hosts the handler we want to fuzz and create example slices to feed into the Seed Corpus. After marshaling the slices into JSON format, we will add them to the fuzzer.
Implementing the Fuzz Target
To start the fuzzing process, we call f.Fuzz(), which accepts a function that acts as the fuzz target. This function should manage error checking, data preparation, and invocation of the function being fuzzed.
The input function for Fuzz should accept testing.T as the first parameter, followed by the data types corresponding to the seed inputs. In our case, we only need to pass testing.T and []byte, as those are the types we added.
Let’s define the Fuzz target to:
- Post the data to our handler.
- Verify the response status.
- Ensure the response is of type int.
While it might seem logical to check the returned value for correctness, it’s often unpredictable since the fuzzer generates random inputs.
Running the Fuzzer
To execute the fuzzer, use the standard Go test command with the --fuzz=Fuzz flag. The value after --fuzz= serves as a prefix for all methods starting with "Fuzz". For example, to fuzz only FuzzTestHTTPHandler, run:
go test --fuzz=FuzzTestHTTPHandler
Fuzzing behaves differently than standard tests; by default, it continues running until an error occurs. You can either cancel it manually or specify a duration limit using the -fuzztime flag. For a 10-second duration, you would execute:
go test --fuzz=Fuzz -fuzztime=10s
Wait for the fuzzer to fail, and you should see output indicating the nature of the failure.
Fuzzing In Go - YouTube: This video discusses fuzz testing in Go, detailing its implementation and benefits.
Analyzing Fuzz Test Failures
When an error occurs, the fuzzer will save the input parameters that caused the failure in a designated file. For example, if the JSON marshaling fails, you can inspect the contents of the failure file.
This experience is common when initially writing fuzz tests. To enhance the quality of the generated data, we should only allow valid JSON to be processed. We can skip invalid payloads using t.Skip("reason for skipping"). To ensure that we only pass valid JSON to the handler, we can use the json.Valid function for preliminary checks.
To further validate the JSON, we can attempt to unmarshal the data into a predefined struct and skip any payloads that fail. After making these adjustments, you can rerun the fuzzer:
go test -v --fuzz=Fuzz
Eventually, you should see a new failure file indicating the issue detected by the fuzzer.
packagemain #23: Fuzz Testing in Go - YouTube: This video explores practical fuzz testing examples in Go, demonstrating its effectiveness.
Conclusion
You are now equipped to write your own fuzz tests. Although the example we examined may seem trivial, fuzz testing for HTTP handlers and other methods can uncover hard-to-detect bugs. Fuzzers often reveal issues that traditional unit tests might overlook, as unit tests usually involve predictable input values.
As demonstrated, crafting a fuzzer for an HTTP handler is relatively straightforward, and with practice, you can quickly implement new fuzzers, enhancing the robustness of your codebase.
What are you waiting for? Dive in and start fuzz testing today!
For further insights into the changes in Go 1.18, feel free to check out my other article on using the new generics.