When Unit Tests Lie and Bugs Remain
You wrote 20 unit tests. All green. CI passes. You're satisfied. And then — a bug in production. Sound familiar? Classic scenario: you only tested the cases you thought of yourself. But reality, as luck would have it, throws in combinations you never even considered.
In Go, there is a powerful but underappreciated approach — property-based testing. Instead of checking specific inputs and outputs, you describe the properties that a function should satisfy, and the framework itself generates hundreds of random test data sets. If a property is violated — you've found a bug.
In this article, we'll break down how to implement property-based testing in Go using the gopter library, write real examples, and see what bugs can be caught. Let's go!
What is Property-based Testing and Why Do You Need It?
Imagine you are testing a sorting function. A typical unit test:
func TestSort(t *testing.T) { input := []int{3, 1, 2} result := sort(input) expected := []int{1, 2, 3} if !reflect.DeepEqual(result, expected) { t.Errorf("got %v, want %v", result, expected) }}Okay, it works. But what if the array is empty? What if all elements are the same? What if there are negative numbers? You could write 10 more tests, but you'll still miss something.
Property-based testing solves this problem radically: you formulate invariants — properties that must be true for any valid input. For example, for sorting:
- The length of the sorted array equals the length of the original.
- Every element from the original array is present in the sorted array (and vice versa).
- The array is sorted in ascending order.
The framework (e.g., gopter or the built-in testing/quick) generates random input data and checks that all properties hold. If there's a bug — you'll find out about it, even without knowing where to look.
Go Libraries for Property-based Testing
The Go ecosystem has several tools:
testing/quick— a built-in package, minimalistic but with limited capabilities.gopter— a powerful library with generators, shrinking (input data minimization), and flexible settings. We recommend this one.rapid— a modern, more performant alternative.
We'll use gopter because it has the best documentation and community. Installation:
go get github.com/leanovate/gopterWriting Your First Property-based Test in Go
Let's say we have a function Reverse that reverses a string:
func Reverse(s string) string { runes := []rune(s) for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 { runes[i], runes[j] = runes[j], runes[i] } return string(runes)}What property can we check? If you reverse a string twice, you should get the original back:
import ( "testing" "github.com/leanovate/gopter" "github.com/leanovate/gopter/gen")
func TestReverseTwice(t *testing.T) { properties := gopter.NewProperties(nil)
properties.Property("reverse(reverse(s)) == s", prop.ForAll( func(s string) bool { return Reverse(Reverse(s)) == s }, gen.AnyString(), ))
properties.TestingRun(t)}Run the test — and gopter will generate 100 random strings and check the property. If everything is fine — the test is green. If not — you'll get a counterexample (the smallest string on which the test failed).