nw-pbt-go

Go property-based testing with rapid and gopter frameworks

321 stars

Best use case

nw-pbt-go is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Go property-based testing with rapid and gopter frameworks

Teams using nw-pbt-go should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/nw-pbt-go/SKILL.md --create-dirs "https://raw.githubusercontent.com/nWave-ai/nWave/main/nWave/skills/nw-pbt-go/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/nw-pbt-go/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How nw-pbt-go Compares

Feature / Agentnw-pbt-goStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Go property-based testing with rapid and gopter frameworks

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# PBT Go -- rapid, gopter

## Framework Selection

| Framework | Shrinking | Stateful | API Style | Choose When |
|-----------|-----------|----------|-----------|-------------|
| rapid | Internal (Hypothesis-like) | Yes (StateMachine) | Idiomatic Go (`*rapid.T`) | Default choice. Simpler API, better shrinking. |
| gopter | Type-based | Yes (commands) | Explicit generator construction | Need maximum control over generation |

**Default**: rapid. More idiomatic Go API and fully automatic shrinking.

## Quick Start (rapid)

```go
import (
    "sort"
    "testing"
    "pgregory.net/rapid"
)

func TestSortPreservesLength(t *testing.T) {
    rapid.Check(t, func(t *rapid.T) {
        xs := rapid.SliceOf(rapid.Int()).Draw(t, "xs")
        sorted := make([]int, len(xs))
        copy(sorted, xs)
        sort.Ints(sorted)
        if len(sorted) != len(xs) {
            t.Fatalf("length changed: %d -> %d", len(xs), len(sorted))
        }
    })
}

// Run: go test
```

## Generator Cheat Sheet (rapid)

### Primitives
```go
rapid.Int()                           // any int
rapid.IntRange(0, 99)                 // bounded
rapid.Int32()
rapid.Float64()
rapid.String()
rapid.StringN(1, 50, -1)             // min 1, max 50 chars
rapid.Bool()
rapid.Byte()
```

### Collections
```go
rapid.SliceOf(rapid.Int())
rapid.SliceOfN(rapid.Int(), 1, 10)   // min 1, max 10 elements
rapid.MapOf(rapid.String(), rapid.Int())
```

### Combinators
```go
rapid.OneOf(rapid.Int(), rapid.Int32()) // does not work for different types
rapid.SampledFrom([]string{"a", "b", "c"})
rapid.Just(42)

// Map (transform)
rapid.Map(rapid.Int(), func(x int) int { return x * 2 }) // even integers

// Filter
rapid.Filter(rapid.Int(), func(x int) bool { return x > 0 })
// Prefer: rapid.IntRange(1, math.MaxInt)

// Custom generator (draw pattern)
func userGen(t *rapid.T) User {
    return User{
        Name: rapid.StringN(1, 20, -1).Draw(t, "name"),
        Age:  rapid.IntRange(1, 120).Draw(t, "age"),
    }
}
// Use: rapid.Custom(userGen)
```

## Stateful Testing (rapid)

```go
type storeModel struct {
    items map[string]int
}

func (m *storeModel) Init(t *rapid.T) {
    m.items = make(map[string]int)
}

func (m *storeModel) Put(t *rapid.T) {
    key := rapid.String().Draw(t, "key")
    val := rapid.Int().Draw(t, "val")
    store.Put(key, val)       // real system
    m.items[key] = val        // model
}

func (m *storeModel) Get(t *rapid.T) {
    if len(m.items) == 0 {
        t.Skip("no items")   // precondition
    }
    keys := make([]string, 0, len(m.items))
    for k := range m.items {
        keys = append(keys, k)
    }
    key := rapid.SampledFrom(keys).Draw(t, "key")
    got := store.Get(key)
    if got != m.items[key] {
        t.Fatalf("get(%q): expected %d, got %d", key, m.items[key], got)
    }
}

func (m *storeModel) Check(t *rapid.T) {
    if store.Size() != len(m.items) {
        t.Fatalf("size mismatch: %d vs %d", store.Size(), len(m.items))
    }
}

func TestStore(t *testing.T) {
    rapid.Check(t, rapid.Run[*storeModel]())
}
```

No parallel/linearizability testing in rapid.

## Stateful Testing (gopter)

gopter provides stateful testing via `commands` package:

```go
import (
    "github.com/leanovate/gopter"
    "github.com/leanovate/gopter/commands"
    "github.com/leanovate/gopter/gen"
)

var storeCommands = &commands.ProtoCommands{
    NewSystemUnderTestFunc: func(initialState commands.State) commands.SystemUnderTest {
        return NewMyStore()
    },
    InitialStateGen: gen.Const(map[string]int{}),
    GenCommandFunc: func(state commands.State) gopter.Gen {
        return gen.OneGenOf(
            gen.Struct(reflect.TypeOf(&PutCommand{}), map[string]gopter.Gen{
                "Key": gen.AlphaString(),
                "Val": gen.Int(),
            }),
            gen.Struct(reflect.TypeOf(&GetCommand{}), map[string]gopter.Gen{
                "Key": gen.AlphaString(),
            }),
        )
    },
}

func TestStoreStateful(t *testing.T) {
    parameters := gopter.DefaultTestParameters()
    properties := gopter.NewProperties(parameters)
    properties.Property("store model", commands.Prop(storeCommands))
    properties.TestingRun(t)
}
```

## Generator Cheat Sheet (gopter)

```go
gen.Int()                             // any int
gen.IntRange(0, 100)                  // bounded
gen.Float64()
gen.AlphaString()                     // alphabetic string
gen.AnyString()
gen.Bool()
gen.SliceOf(gen.Int())                // []int
gen.MapOf(gen.AlphaString(), gen.Int())
gen.OneConstOf("a", "b", "c")        // pick from values
gen.OneGenOf(gen.Int(), gen.Int64())  // union of generators
gen.Frequency(
    gen.NewWeightedGen(80, gen.Int()),
    gen.NewWeightedGen(20, gen.Const(0)),
)
gen.Struct(reflect.TypeOf(&User{}), map[string]gopter.Gen{
    "Name": gen.AlphaString(),
    "Age":  gen.IntRange(1, 120),
})
```

## Quick Start (gopter)

```go
func TestSortLength(t *testing.T) {
    properties := gopter.NewProperties(nil)
    properties.Property("sort preserves length", prop.ForAll(
        func(xs []int) bool {
            sorted := make([]int, len(xs))
            copy(sorted, xs)
            sort.Ints(sorted)
            return len(sorted) == len(xs)
        },
        gen.SliceOf(gen.Int()),
    ))
    properties.TestingRun(t)
}
```

## Test Runner Integration

```go
// rapid: go get pgregory.net/rapid
// gopter: go get github.com/leanovate/gopter

// Both integrate with standard go test
// Run: go test ./...
// rapid saves failures to testdata/ for replay
```

## Unique Features

### rapid
- **Idiomatic Go**: Uses `*rapid.T` like Go's `*testing.T`
- **Internal shrinking**: Fully automatic, no user code needed
- **Draw pattern**: Generators called inline (`rapid.Int().Draw(t, "name")`)
- **StateMachine**: Methods on struct define commands; `Init`, `Check` are special

### gopter
- **commands package**: Traditional state machine testing with pre/post conditions
- **gen.Struct**: Generate structs with per-field generators
- **Derived generators**: `DeriveGen` for automatic struct generation

Related Skills

nw-ux-web-patterns

322
from nWave-ai/nWave

Web UI design patterns for product owners. Load when designing web application interfaces, writing web-specific acceptance criteria, or evaluating responsive designs.

nw-ux-tui-patterns

322
from nWave-ai/nWave

Terminal UI and CLI design patterns for product owners. Load when designing command-line tools, interactive terminal applications, or writing CLI-specific acceptance criteria.

nw-ux-principles

322
from nWave-ai/nWave

Core UX principles for product owners. Load when evaluating interface designs, writing acceptance criteria with UX requirements, or reviewing wireframes and mockups.

nw-ux-emotional-design

322
from nWave-ai/nWave

Emotional design and delight patterns for product owners. Load when designing onboarding flows, empty states, first-run experiences, or evaluating the emotional quality of an interface.

nw-ux-desktop-patterns

322
from nWave-ai/nWave

Desktop application UI patterns for product owners. Load when designing native or cross-platform desktop applications, writing desktop-specific acceptance criteria, or evaluating panel layouts and keyboard workflows.

nw-user-story-mapping

322
from nWave-ai/nWave

User story mapping for backlog management and outcome-based prioritization. Load during Phase 2.5 (User Story Mapping) to produce story-map.md and prioritization.md.

nw-tr-review-criteria

322
from nWave-ai/nWave

Review dimensions and scoring for root cause analysis quality assessment

nw-tlaplus-verification

322
from nWave-ai/nWave

TLA+ formal verification for design correctness and PBT pipeline integration

nw-test-refactoring-catalog

322
from nWave-ai/nWave

Detailed refactoring mechanics with step-by-step procedures, and test code smell catalog with detection patterns and before/after examples

nw-test-organization-conventions

322
from nWave-ai/nWave

Test directory structure patterns by architecture style, language conventions, naming rules, and fixture placement. Decision tree for selecting test organization strategy.

nw-test-design-mandates

322
from nWave-ai/nWave

Four design mandates for acceptance tests - hexagonal boundary enforcement, business language abstraction, user journey completeness, walking skeleton strategy, and pure function extraction

nw-tdd-review-enforcement

322
from nWave-ai/nWave

Test design mandate enforcement, test budget validation, 5-phase TDD validation, and external validity checks for the software crafter reviewer