rapid turns Go tests into property checks with automatic shrinking
Rapid is a modern Go property-based testing library
At a glance
- What is it?
- rapid is a Go property-based testing library that checks properties you define over automatically generated test cases and shrinks a failing case to the smallest form without any user code. It has no dependencies outside the standard library, it supports state machine testing, and it can hand any rapid test to the standard fuzzer.
- Who is it for?
- Adopt rapid if you write Go and want properties, not examples, covering your parsers, encoders, state machines and data structures, and if you want failures that arrive already shrunk to a small reproducible case.
- Can I use it commercially?
- Yes, with conditions. MPL-2.0 is a weak copyleft licence: you can use it inside commercial and closed-source software, but if you distribute changes to its own files, you must publish those changes under the same licence.
- Is it still maintained?
- Yes. The repository last received commits 49 days ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 21, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Properties over examples, with the failure shrunk for you
rapid is a Go library for property-based testing, and the model is a shift in what a test asserts. Instead of writing example inputs and expected outputs, you write a property, a statement that should hold for arbitrary input, and the library generates the input for you.
The README describes the loop in one line: rapid checks that properties you define hold for a large number of automatically generated test cases, and if a failure is found it automatically minimizes the failing test case before presenting it. That second clause is the part that separates rapid from a naive generator. A property test that fails with a 400 element slice of integers is close to useless. A property test that fails with a three element slice is a bug report.
The feature list makes the scope concrete: an imperative Go API with type-safe data generation using generics, data generation biased to explore small values and edge cases more thoroughly, fully automatic minimization of failing test cases, persistence and automatic re-running of minimized failing test cases, support for state machine testing, and no dependencies outside the Go standard library.
That last point deserves emphasis. A testing library that pulls nothing from outside the standard library does not add to your dependency graph, does not create version conflicts, and does not need auditing. For a library that runs inside your test suite on every commit, that is a real property rather than a nicety.
The whole API in one test function
The README shows what a trivial rapid test looks like, and the shape is deliberately plain Go. The test is an ordinary testing function, and the property is a closure:
func TestSortStrings(t *testing.T) {
rapid.Check(t, func(t *rapid.T) {
s := rapid.SliceOf(rapid.String()).Draw(t, "s")
slices.Sort(s)
if !slices.IsSorted(s) {
t.Fatalf("unsorted after sort: %v", s)
}
})
}The module is imported as pgregory.net/rapid, and the test file is an external test package, rapid_test, in the examples. The pieces are few. rapid.Check runs the property many times with different generated data. Inside the closure, rapid.SliceOf(rapid.String()) builds a generator for a slice of strings, and Draw(t, "s") draws one value and labels it, which is what lets the failure report tell you which part failed. Checking is ordinary code: an if statement and t.Fatalf, exactly as the README puts it.
A more complete example is the arithmetic property test, where the generated values are drawn into named variables and then a series of properties are checked:
rapid.Check(t, func(t *rapid.T) {
var (
a = rapid.Int().Draw(t, "a")
b = rapid.Int().Draw(t, "b")
c = rapid.Int().Draw(t, "c")
)
if add(a, 0) != a {
t.Fatalf("add() does not have 0 as identity")
}
if add(a, b) != add(b, a) {
t.Fatalf("add() is not commutative")
}
if add(a, add(b, c)) != add(add(a, b), c) {
t.Fatalf("add() is not associative")
}That is the whole idea in twenty lines. The properties are the algebraic laws of addition, and the library searches for inputs that break them.
How the bitstream becomes a value, and how a value shrinks
The FAQ section of the README explains the mechanism, and it is the part worth understanding if you want to reason about the failures you get.
A Generator is a specification, not a value. When you construct one, nothing happens. When you call Draw, rapid takes some bytes from its internal random bitstream and uses them to construct the value based on the generator specification, while tracking how the random bytes used correspond to the value and its subparts. That tracking is the key. The knowledge about the structure of the generated values, and their relationship with the parts of the bitstream, is what allows rapid to minimize a failure intelligently and automatically.
So the shrinking is not a separate heuristic bolted onto the generator. It falls out of keeping a record of which bytes produced which part of the value.
The repository layout follows that structure. generator.go holds the generator type and the drawing logic, data.go the base data generation, engine.go the check engine, shrink.go the minimization, and vis.go the visualisation of generated and shrunk values. The per type files, integers.go, floats.go, strings.go and collections.go, provide the concrete generators, and combinators.go composes them, which is how SliceOf(rapid.String()) can be built from a string generator.
Two more files complete the picture. make.go covers the MakeFuzz bridge to the standard fuzzer, and persist.go handles the persistence of failing cases. The test files are named consistently, with example files for the public API, external test files for the package surface, and dedicated engine, shrink and persist test files.
Generation is biased towards small values and edge cases
The feature list says the data generation is biased to explore small values and edge cases more thoroughly, and this is the property that makes rapid useful rather than merely different from example-based tests.
The reason is practical. If values were drawn uniformly, almost every generated integer would be a large number, and the interesting behaviour of most code lives near zero, at the boundaries, and at the edges of collections. A generator that spends its budget near those places finds more distinct bugs per second of test time.
The API surface is the one you would expect from the feature list. rapid.Int() produces integers, and the data file holds the primitive generators, with the collection helpers in collections.go. The combinators file is what lets you build composite generators, since the SliceOf call in the README example is a combinator over a string generator. The strings.go, integers.go and floats.go files cover the scalar cases, and the repository includes a regexp external test file exercising regex-based generation.
For stateful code, the repository has statemachine.go and a Queue state machine example, and the README comparison section points at the source code for that example in both rapid and gopter, so the two approaches can be read side by side. The rapid example is a state machine test in example_statemachine_test.go, and the gopter one is linked from the gopter repository as a circular queue test.
The persistence behaviour is what closes the loop. When a failing case is found, it is minimized and then written out, and on the next run the persisted case is re-run automatically. That means a bug found once does not have to be rediscovered, and a test that failed yesterday still fails this morning even if the random data would have missed it.
Flags and environment variables for the same knobs
rapid does not need a separate runner. The README says to just run go test as usual, and it will pick up all rapid tests as well. The library is invisible to the ordinary Go test workflow, which is what makes it easy to adopt incrementally.
The behaviour is tuned through flags with a -rapid. prefix, and they are discovered the same way as any other test flag, by running go test -args -h and looking for the -rapid. prefixed entries. The example given is:
go test -rapid.checks=10_000The number of checks is therefore something you can raise in CI without touching a line of test code, which is the point of exposing it as a flag rather than a constant.
Every flag also has a matching environment variable, with the dot replaced by an underscore and the name uppercased, so -rapid.checks corresponds to RAPID_CHECKS. That mapping is stated in the README.
Two flags are worth calling out by name because they change the character of the search. The number of checks controls how many generated cases are tried per property, and it is the flag you are most likely to raise in CI. If you want to know the exact set available in your version, go test -args -h is the command that answers it, and the flags are listed there with their defaults.
How rapid compares to testing.F.Fuzz, gopter and testing/quick
The README has a comparison section, and it is unusually candid. Each entry states what rapid gains and what it gives up.
Against testing.F.Fuzz, the standard library fuzzer, rapid shines in generating complex structured data, including state machine tests, but lacks coverage-guided feedback and mutations. The README also notes the bridge: with MakeFuzz, any rapid test can be used as a fuzz target for the standard fuzzer. So the two are not competing, they are layered, and the property test you already wrote becomes an entry point for the coverage guided search.
Against gopter, rapid provides a much simpler API, is much smarter about data generation, and is able to minimize failing test cases fully automatically, without any user code. The comparison is anchored on the same kind of test, a queue, implemented in both libraries, so the difference is visible rather than asserted.
Against testing/quick, the older standard library checker, rapid has convenient data generation facilities and a form of test case minimization, which the README names as the two main things to look for in a property-based testing library.
The FAQ then explains the difference in kind between property-based testing and fuzzing. Property-based testing focuses on a quick feedback loop, checking properties on a small but diverse set of pseudo-random inputs in a fraction of a second. Fuzzing focuses on slow, often multi-day, brute force input generation that maximizes coverage. Both are useful, property-based tests are used alongside regular example-based tests during development, and fuzzing is used to search for edge cases and security vulnerabilities.
MPL-2.0, Go 1.23, and a release every year or two
The licence is MPL-2.0, and the module file is about as small as a module file gets. The module path is pgregory.net/rapid, the language version is go 1.23, and there are no require blocks at all, which is the machine readable form of the promise that rapid has no dependencies outside the Go standard library.
The release history is steady but unhurried. The recent tags are v1.3.0 on 2026-04-30, v1.2.0 on 2025-02-26 and v1.1.0 on 2023-08-08, and the last push to the repository was on 2026-09-04. The repository is not archived. A library that ships roughly one minor release a year and has stayed on a 1.x line since 2023 is a reasonable signal for a testing library, since the API of a generator library is not expected to churn.
Two details in the repository tell you how the work is organised. There is a TODO.md file at the root, which is where a project of this kind keeps its roadmap, and a CONTRIBUTING.md. The source tree is flat and file-per-concern rather than nested by package, with files named after the concept they implement, so the test files mirror the source files one for one.
There is also a synctest_enabled.go and a synctest_disabled.go pair, with matching test files, which is a build-tag split for the newer testing facilities in the Go standard library. That is a small but real signal about the project tracking the language rather than lagging behind it, and it is the sort of detail you only notice by reading the repository.
The author also maintains chaos_theory, a Rust alternative to rapid, and the README points people who like rapid but work in Rust toward it. For a Go project that is irrelevant, but for anyone choosing a property-based testing approach across two languages, it tells you the ideas are portable even if the implementation is not.
Editorial conclusion
Adopt rapid if you write Go and want properties, not examples, covering your parsers, encoders, state machines and data structures, and if you want failures that arrive already shrunk to a small reproducible case. Do not adopt it as a replacement for coverage-guided fuzzing, since the README states that rapid lacks coverage-guided feedback and mutations, and do not adopt it in a project that cannot move to Go 1.23 or later, because that is the language version the module declares. Before you start, run a rapid test with go test -rapid.checks=10_000 to see how the data distribution behaves on your own code, read the Queue state machine example, and check the shrink.go and persist.go behaviour so you know where the failure artifacts are written and re-run.
Frequently asked questions
What is rapid and what does it do in a Go test?
rapid is a Go library for property-based testing. It checks properties you define over many automatically generated test cases and, if a failure is found, minimizes the failing case before presenting it, with no user code required for the minimization.
Does rapid have any dependencies outside the Go standard library?
No. The feature list states no dependencies outside the Go standard library, and the module file declares the module pgregory.net/rapid with go 1.23 and no require blocks at all.
How do I run a rapid test and control how many cases it checks?
Run go test as usual, since rapid tests are picked up by the standard test runner. Tune the behaviour with flags prefixed -rapid., for example go test -rapid.checks=10_000, and each flag has a matching environment variable with the dot replaced by an underscore, such as RAPID_CHECKS.
How does rapid compare with testing.F.Fuzz and gopter?
Against testing.F.Fuzz, rapid generates complex structured data including state machine tests but lacks coverage-guided feedback and mutations, and any rapid test can be used as a fuzz target through MakeFuzz. Against gopter, rapid has a simpler API and fully automatic minimization of failing cases.
Can rapid test state machines?
Yes. The feature list includes support for state machine testing, and the repository holds a Queue state machine example in example_statemachine_test.go alongside the statemachine.go source file.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/flyingmutant-rapid)