full_system_example

package
v0.0.9 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 12, 2026 License: MIT Imports: 1 Imported by: 0

README

Full system example: payment service

This example demonstrates all major features of the go-specs testing framework using a small payment system with a deliberate bug. It is suitable for documentation and as a reference for BDD, path exploration, shrinking, mocks, and snapshots.

System overview

The payment service provides:

  • Deposit(balance, amount) — returns balance + amount
  • Withdraw(balance, amount) — returns the new balance; contains a bug when amount > balance
  • Transfer(ledger, fromBalance, toBalance, amount) — moves amount from source to destination and optionally records via a Ledger (external dependency)

The Ledger interface is mocked in tests so we can verify that RecordTransfer(from, to, amount) is called with the expected arguments.

Run the example

go test ./examples/full_system_example

Because Withdraw has a bug, the test run will:

  1. Explore the input space (balance and amount from 0 to 1000) using ExploreSmart (boundary values, random sampling, coverage-guided mutation).
  2. Discover a failing case (e.g. amount > balance producing a negative balance).
  3. Shrink the failing input to a minimal case and report it.

Example output:

FAIL after 11 tests

minimal failing input:

balance = 0
amount = 1

Path exploration

Path specs define dimensions and let the framework generate combinations:

s.Paths(func(p *specs.PathBuilder) {
    p.IntRange("balance", 0, 1000)
    p.IntRange("amount", 0, 1000)
}).ExploreSmart(5000).It("never produces negative balance", func(ctx *specs.Context) {
    balance := ctx.Path().Int("balance")
    amount := ctx.Path().Int("amount")
    newBalance := Withdraw(balance, amount)
    ctx.Expect(newBalance >= 0).To(specs.BeTrue())
})
  • IntRange defines an integer dimension (min, max).
  • ExploreSmart(5000) runs up to 5000 inputs using smart exploration (boundaries, random, coverage, corpus).
  • ctx.Path() gives the current combination (e.g. balance, amount).

Other options: .It(...) for full Cartesian enumeration, .Sample(n) for random sampling, .ExploreCoverage(n) for coverage-guided exploration.

Automatic bug discovery

The property “withdraw never produces negative balance” is violated when amount > balance. The framework:

  1. Tries many inputs (ExploreSmart).
  2. Stops when an assertion fails.
  3. Runs the shrinker to reduce the failing input to a minimal one (e.g. balance=0, amount=1).
  4. Reports that minimal case in the failure message.

No need to hand-pick the failing case; exploration and shrinking do it.

Shrinking

When a path spec fails, go-specs runs a shrinker that:

  • Takes the failing PathValues.
  • Shrinks each dimension in turn (e.g. binary search toward zero for integers).
  • Re-runs the test for each candidate; if it still fails, keeps the smaller input.
  • Stops when no smaller failing input is found.

The failure message shows this minimal failing input so you can fix the bug with a small, reproducible example.

Mock verification

The Ledger dependency is replaced by a mock that records calls:

m := mock.New()
ledger := NewMockLedger(m)
svc := NewTransferService(ledger)

svc.Transfer(100, 50, 20)

recordSpy := m.Spy("RecordTransfer")
ctx.Expect(recordSpy.CallCount()).ToEqual(1)
if !recordSpy.CalledWith(mock.Equal(100), mock.Equal(50), mock.Equal(20)) {
    t.Fatal("expected RecordTransfer(100, 50, 20)")
}
  • NewMockLedger(m) returns a Ledger that forwards RecordTransfer to m.Spy("RecordTransfer").
  • Spy.CallCount() and Spy.CalledWith(matchers...) verify that the right call happened.

Snapshots

Structured results can be captured and compared to stored snapshots:

result := map[string]any{"fromBalance": from, "toBalance": to, "amount": 25}
ctx.Snapshot("transfer_result", result)

Snapshots are stored under __snapshots__/<test_file>.snap.json. To create or update them:

GO_SPECS_UPDATE_SNAPSHOTS=1 go test ./examples/full_system_example

Fixing the bug

To make the example pass, fix Withdraw in payment.go so that when amount > balance the function does not return a negative value (e.g. return balance unchanged or return an error). After that, go test ./examples/full_system_example should pass.

Files

File Purpose
payment.go Deposit, Withdraw (with bug), Transfer + Ledger call
payment_mocks.go Ledger interface, TransferService, NewMockLedger
payment_test.go BDD specs: Deposit, Transfer (mocks), snapshot, Withdraw (ExploreSmart)
README.md This overview

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Deposit

func Deposit(balance, amount int) int

Deposit returns balance + amount (no cap in this example).

func Transfer

func Transfer(ledger Ledger, fromBalance, toBalance, amount int) (int, int)

Transfer moves amount from fromBalance to toBalance and records via the ledger. Returns (newFromBalance, newToBalance). Fails if fromBalance < amount (no change).

func Withdraw

func Withdraw(balance, amount int) int

Withdraw returns the new balance after withdrawing amount. Never returns negative; if amount > balance, returns 0.

Types

type Ledger

type Ledger interface {
	RecordTransfer(from, to, amount int)
}

Ledger records transfer operations (external dependency to mock).

func NewMockLedger

func NewMockLedger(m *mock.Mock) Ledger

NewMockLedger returns a Ledger that records RecordTransfer calls to m.Spy("RecordTransfer"). Use it to verify transfer interactions: m.Spy("RecordTransfer").CalledWith(mock.Equal(from), mock.Equal(to), mock.Equal(amount)).

type TransferService

type TransferService struct {
	// contains filtered or unexported fields
}

TransferService performs transfers and notifies the ledger.

func NewTransferService

func NewTransferService(ledger Ledger) *TransferService

NewTransferService returns a transfer service that uses the given ledger.

func (*TransferService) Transfer

func (s *TransferService) Transfer(fromBalance, toBalance, amount int) (newFrom, newTo int)

Transfer moves amount from fromBalance to toBalance, updates balances, and records via the ledger.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL