From golang-workflow
Property-based testing patterns using testing/quick and custom generators
npx claudepluginhub jamesprial/prial-plugins --plugin golang-workflowThis skill uses the workspace's default tool permissions.
Property-based testing verifies that invariants hold across many automatically generated inputs, rather than checking specific input/output pairs.
Implements property-based testing in Go with rapid (default) and gopter frameworks. Includes generator cheat sheets, stateful examples, quick starts, and framework comparison.
Generative testing with QuickCheck/Hypothesis; testing properties that should hold for all inputs.
Guides property-based testing for serialization, validation, normalization, and pure functions with property catalog, pattern detection, and library references.
Share bugs, ideas, or general feedback.
Property-based testing verifies that invariants hold across many automatically generated inputs, rather than checking specific input/output pairs.
import "testing/quick"
func Test_Encode_Decode_RoundTrip(t *testing.T) {
roundTrip := func(input string) bool {
encoded := Encode(input)
decoded, err := Decode(encoded)
if err != nil {
return false
}
return decoded == input
}
if err := quick.Check(roundTrip, nil); err != nil {
t.Error(err)
}
}
| Property | Definition | Example |
|---|---|---|
| Round-trip | Decode(Encode(x)) == x | Serialization, compression |
| Idempotent | F(F(x)) == F(x) | Normalization, formatting |
| Commutative | F(a, b) == F(b, a) | Set operations, merge |
| Monotonic | a < b implies F(a) <= F(b) | Sorting, ranking |
| Invariant | P(F(x)) == true for all x | Length bounds, type constraints |
| Inverse | F(x) and G(x) undo each other | Add/Remove, Push/Pop |
When testing/quick can't generate your types automatically:
// Implement quick.Generator for custom types
func (o Order) Generate(rand *rand.Rand, size int) reflect.Value {
items := make([]Item, rand.Intn(size)+1)
for i := range items {
items[i] = Item{
SKU: fmt.Sprintf("SKU-%d", rand.Intn(1000)),
Price: rand.Float64() * 100,
}
}
return reflect.ValueOf(Order{
ID: fmt.Sprintf("ORD-%d", rand.Intn(10000)),
Items: items,
})
}
// Increase iterations for thorough checking
config := &quick.Config{
MaxCount: 1000, // default is 100
MaxCountScale: 0, // multiplier
Rand: rand.New(rand.NewSource(42)), // deterministic for CI
}
if err := quick.Check(property, config); err != nil {
t.Error(err)
}
Property tests complement table-driven tests — they don't replace them:
// Table test: specific known cases
func Test_Sort_KnownCases(t *testing.T) {
tests := []struct{ input, want []int }{
{[]int{3, 1, 2}, []int{1, 2, 3}},
{nil, nil},
}
// ...
}
// Property test: invariants over random input
func Test_Sort_Properties(t *testing.T) {
sorted := func(input []int) bool {
result := Sort(input)
// Property 1: output is same length
if len(result) != len(input) { return false }
// Property 2: output is ordered
for i := 1; i < len(result); i++ {
if result[i] < result[i-1] { return false }
}
return true
}
if err := quick.Check(sorted, nil); err != nil {
t.Error(err)
}
}