danmuji

package module
v0.5.0 Latest Latest
Warning

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

Go to latest
Published: Jun 25, 2026 License: MIT Imports: 17 Imported by: 0

README

Danmuji

A BDD testing language for Go. Write expressive test specs in .dmj files, compile them to standard go test code. No runtime library, no reflection, no magic.

Built on gotreesitter — a pure-Go Tree-sitter implementation with grammar composition.

package cart_test

import "testing"

unit "ShoppingCart.Add" {
    given "an empty cart" {
        cart := NewCart()

        when "adding an item" {
            cart.Add("widget")

            then "count increases" {
                expect cart.Count() == 1
            }
        }
    }
}

This compiles to:

package cart_test

import (
    "testing"
    "github.com/stretchr/testify/assert"
)

func TestShoppingCartAdd(t *testing.T) {
    t.Run("an empty cart", func(t *testing.T) {
        cart := NewCart()
        t.Run("adding an item", func(t *testing.T) {
            cart.Add("widget")
            t.Run("count increases", func(t *testing.T) {
                assert.Equal(t, 1, cart.Count())
            })
        })
    })
}

Run with go test -v. That's it.

Install

go install github.com/odvcencio/danmuji/cmd/danmuji@latest

Usage

# Transpile a single file
danmuji build ./path/to/user_test.dmj

# Transpile all .dmj files in a directory tree
danmuji build ./mypackage/

# Run the generated tests
go test -v ./mypackage/...

Directory builds recurse into subdirectories. Each .dmj file produces a _danmuji_test.go file in the same directory. Put .dmj files next to your Go code, just like _test.go files.

Run tests directly
# Transpile and run in one step (cleans up generated files after)
danmuji test ./mypackage/

# Forward flags to go test
danmuji test ./mypackage/ -- -v -run TestAuth

# Build without //line directives (for debugging generated code)
danmuji build --debug ./mypackage/

When a test fails, errors reference your .dmj source file and line number directly.

Features

Test categories
unit "fast logic" { ... }           // go test ./...
integration "database" { ... }      // go test -tags=integration ./...
e2e "full flow" { ... }             // go test -tags=e2e ./...

integration and e2e blocks emit //go:build tags. Plain go test only runs unit tests. For predictable filtering, keep tagged specs in their own .dmj files.

Given / When / Then
unit "UserService.Create" {
    given "valid input" {
        svc := NewUserService(repo)

        when "creating a user" {
            user, err := svc.Create("alice")

            then "succeeds" {
                expect err == nil
            }
            then "sets name" {
                expect user.Name == "alice"
            }
        }
    }
}

Each block becomes a t.Run subtest. Nest them as deep as you want.

Assertions
expect x == 1                  // assert.Equal(t, 1, x)
expect x != 0                  // assert.NotEqual(t, 0, x)
expect err == nil              // require.NoError(t, err)
expect result is_nil           // assert.Nil(t, result)
expect result not_nil          // assert.NotNil(t, result)
expect items contains "apple"  // assert.Contains(t, items, "apple")
expect items unordered_equal []int{3, 2, 1}
expect err is context.DeadlineExceeded
expect err message contains "quota"
reject ok                      // assert.False(t, ok)

Backed by testify. expect err == nil uses require (stops the test on failure). Everything else uses assert (logs and continues).

Domain-shaped matchers can be defined as plain Go functions:

func hasRole(user string, role string) bool {
	return user == role
}
unit "auth" {
	then "is admin" {
		expect "admin" hasRole "admin"
	}
}

Partial matching works for structs and maps:

then "user payload is right" {
	expect user matches {
		Role: "admin",
		Email: contains "@example.com",
		DeletedAt: is_nil
	}
}
Factories
factory User {
	defaults { Name: "alice", Role: "member" }
	trait admin { Role: "admin" }
}

unit "authorization" {
	user := build User with admin { Name: "root" }

	then "applies traits and overrides" {
		expect user.Name == "root"
		expect user.Role == "admin"
	}
}

Factories stay DSL-only. build emits normal Go composite literals, so the generated tests stay readable.

Eventual and consistent assertions
unit "retries" {
	then "waits for background work" {
		eventually "job has finished" within 5s {
			expect jobDone
		}
		consistently "job sends once" for 2s {
			reject duplicateSend
		}
	}
}

Duration shorthand literals are supported: 5s, 2.5m, 30ms, 100us, 1h. You can also use full Go expressions like 5 * time.Second. The within and for clauses are optional — omit them to use defaults.

These compile into polling loops so you can express temporal behavior without writing custom helper goroutine scaffolding.

When you want a one-shot channel receive with timeout, use await:

unit "worker completion" {
	await <-jobs within 2s as job

	then "receives the completed job" {
		expect job.Status == "done"
	}
}
Mocks
mock UserRepo {
    FindByID(id int) -> User = User{Name: "stub"}
    Save(u User) -> error = nil
}

unit "service" {
    repo := &mockUserRepo{}
    repo.Save(User{Name: "alice"})

    then "save was called" {
        verify repo.Save called 1 times
    }
}

Generates a struct with call counters and canned return values. No code generation step, no external tool.

Verification supports:

verify repo.Save called 1 times
verify repo.Save called with ("alice")
verify repo.Save not_called
Fakes
fake InMemoryStore {
    Get(key string) -> string {
        return "value"
    }
}

Like mocks, but with real method bodies. Use when you need working behavior, not just return values.

Spies
spy EventBus {
    Publish(topic string)
    Subscribe(topic string) -> error = nil
}

Records all calls and arguments. If inner is set, calls delegate to it. If inner is unset, methods fall back to their declared default return (or the zero value when no default is declared). Use when you want verification plus optional pass-through to a real implementation.

bus := &spyEventBus{inner: realBus}

A spy must declare at least one method. Bare spy Logger declarations are rejected during transpilation.

Lifecycle hooks
unit "database tests" {
    before each {
        db := setupTestDB()
    }
    after each {
        db.Close()
    }
    before all {
        initTestEnvironment()
    }
    after all {
        teardownTestEnvironment()
    }
    // ...
}

before each inlines at the top of each subtest. after each becomes t.Cleanup. before all / after all run once for the enclosing test function.

Tags
@slow
@smoke
integration "heavy test" { ... }

Danmuji test blocks run in parallel by default. Use @serial to opt out when a test must stay sequential. process-backed tests also stay sequential automatically.

@slow adds if testing.Short() { t.Skip() }. @skip skips unconditionally. @parallel is accepted for compatibility and readability, but is redundant now that parallel is the default. Any @identifier is a valid tag.

Scenario-driven tests
unit "AuthMiddleware" {
    each "request scenario" {
        defaults { method: "GET", token: "valid", expect_status: 200 }
        { name: "happy path" }
        { name: "no token",      token: "",       expect_status: 401 }
        { name: "expired token", token: "exp456", expect_status: 401 }
        { name: "wrong role",    token: "guest",  expect_status: 403 }
    } do {
        given scenario.name {
            req := buildRequest(scenario.method, scenario.token)
            rec := httptest.NewRecorder()
            handler.ServeHTTP(rec, req)

            then "correct status" {
                expect rec.Code == scenario.expect_status
            }
        }
    }
}

Each entry inherits from defaults and only specifies what changes. Generates a scenario struct, slice, and for...range with subtests that follow the enclosing test's parallel policy.

HTTP test helpers

When a .dmj file references danmujiHTTP., danmuji injects a small helper set around net/http/httptest:

unit "users handler" {
    handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusCreated)
    })

    req := danmujiHTTP.POST("/users", map[string]string{"name": "alice"})
    rec := danmujiHTTP.Serve(handler, req)

    then "creates users" {
        expect rec.Code == http.StatusCreated
    }
}

Available methods: Request, GET, POST, PUT, PATCH, DELETE, and Serve. String bodies become plain text, []byte bodies stay binary, io.Reader passes through, and other body values are JSON-marshaled automatically with Content-Type: application/json.

WebSocket test helpers

When a .dmj file references danmujiWS., danmuji injects a small helper set around github.com/gorilla/websocket:

unit "theatre sync" {
	server := httptest.NewServer(handler)
	defer server.Close()

	ws := danmujiWS.Dial(server, "/api/theatre/ABC123/ws")
	defer ws.Close()

	then "receives heartbeat" {
		msg := ws.ReadBinary(2 * time.Second)
		expect msg[0] == byte(0x01)
	}

	ws.SendBinary(driftBytes)

	then "records the last message" {
		expect ws.LastMessage() not_nil
	}
}

Available methods: Dial, Close, SendBinary, SendText, ReadBinary, ReadText, and LastMessage.

Matrix tests
unit "API compatibility" {
    matrix "method x auth" {
        method: { "GET", "POST", "PUT", "DELETE" }
        auth: { "none", "basic", "bearer" }
    } do {
        // 4 x 3 = 12 tests, auto-generated
        then "no panic" {
            expect true
        }
    }
}

Cartesian product of all dimensions. Each combination runs as a subtest and follows the enclosing test's parallel policy. Each dimension is also bound as a local alias inside the generated loop, so nested blocks can reference names like method or auth directly.

gRPC test helpers

When a .dmj file references danmujiGRPC., danmuji injects a small helper set for grpc/test/bufconn:

unit "worker rpc" {
	conn := danmujiGRPC.Bufconn(func(s *grpc.Server) {
		pb.RegisterWorkerServer(s, fakeWorker)
	})
	defer conn.Close()

	then "invokes unary grpc" {
		err := conn.Conn().Invoke(ctx, "/pkg.Worker/Ping", req, resp)
		expect err == nil
	}
}

Available methods: Bufconn, Conn, and Close.

Property-based specs
unit "integer rules" {
	property "addition commutative" for all (a int, b int) up to 500 {
		expect a + b == b + a
	}
}

This compiles into testing/quick.Check with a predicate-style function, so the body is evaluated over generated values instead of a fixed example table. The optional up to N clause overrides the default sample count.

Data tables
unit "addition" {
    table cases {
        | 1 | 2 | 3 |
        | 4 | 5 | 9 |
        | 100 | 200 | 300 |
    }
    each row in cases {
        then "adds correctly" {
            expect row.col0.(int) + row.col1.(int) == row.col2
        }
    }
}

Generates a struct per table with col0, col1, ... interface{} fields and iterates with for...range. Generated row subtests follow the enclosing test's parallel policy.

Containers (testcontainers-go)
integration "database round-trip" {
    needs tempdir scratch
    needs postgres db {
        password: "test"
        database: "app_test"
    }
    needs http server {
        handler api.NewHandler(repo)
    }

    given "a connected database" {
        expect scratch != ""
        expect dbEndpoint not_nil
        expect server.URL not_nil
    }
}

Supported services: tempdir, http, postgres, redis, mysql, kafka, mongo, rabbitmq, nats, container (generic). Backed by testcontainers-go for containerized services.

Benchmarks
benchmark "JSON marshal" {
    setup {
        data := makeLargeStruct()
    }
    measure {
        json.Marshal(data)
    }
    report_allocs
}

Generates func BenchmarkJSONMarshal(b *testing.B) with b.ResetTimer(), b.N loop, and b.ReportAllocs(). Run with go test -bench=..

For concurrent benchmarks:

benchmark "concurrent reads" {
    setup {
        cache := NewCache()
    }
    parallel measure {
        cache.Get("key")
    }
}

Generates b.RunParallel.

Load testing (vegeta)
load "checkout endpoint" {
    rate 50
    duration 30s
    rampup 5s
    target post "http://localhost:8080/api/checkout"

    then "fast enough" {
        expect true
    }
}

Generates vegeta attack code with rate limiting, duration, and metrics collection. Load tests get //go:build e2e by default.

Additional load config options: rampup and concurrency. duration and rampup accept duration literals such as 30s, 500ms, or plain expressions.

Profiling
unit "memory check" {
    profile mem {}
    // ... test code
}

unit "goroutine check" {
    profile routines {}
    // ... test code
}

Captures runtime/pprof profiles inline with your tests. Supports: cpu, mem, allocs, routines, blockprofile, mutexprofile.

Profile directives:

  • save "path" writes file-backed profiles to an explicit path.
  • show top N records the request and logs the profile path, but does not shell out to go tool pprof automatically.

routines tracks goroutine deltas inline; it does not currently write a .pprof file.

Goroutine leak detection
unit "connection pool" {
    no_leaks

    given "a pool" {
        pool := NewPool(10)
        pool.Start()
        pool.Stop()
    }
}

One keyword. Captures goroutine count before and after, fails if any leaked.

Fake clock with timezone support
unit "scheduler" {
    fake_clock at "2026-03-17T09:00:00Z" in "America/New_York"

    then "correct timezone" {
        expect clock.Now().Location().String() == "America/New_York"
    }

    then "can advance" {
        clock.Advance(24 * time.Hour)
        expect clock.Now().Day() == 18
    }
}

Three forms: fake_clock at "time" in "tz", fake_clock at "time", or bare fake_clock for a default zero-value clock.

Generates a Clock interface and fakeClock struct with Advance, Set, and SetLocation methods.

Shell commands
unit "migrations" {
    exec "run migrations" {
        run "migrate -database $DB_URL up"
        expect exit_code == 0
        expect stdout contains "applied"
    }
}

Runs shell commands with assertable exit_code, stdout, and stderr.

Snapshot testing
unit "API response" {
    given "a valid user" {
        user := User{Name: "Alice", Email: "alice@test.com"}

        snapshot "user_json" {
            json.Marshal(user)
        }
    }
}

First run creates testdata/snapshots/user_json.golden. Subsequent runs compare against it. Update with DANMUJI_UPDATE_SNAPSHOTS=1 go test ./....

Process testing (opaque box)
e2e "API server" {
    process "./cmd/server" {
        args "--port=9090 --verbose"
        env { DB_URL: "postgres://localhost/test", LOG_LEVEL: "debug" }
        ready http "http://localhost:9090/health"
    }

    then "create user" {
        resp := post("http://localhost:9090/users", body)
        expect resp.StatusCode == 201
    }

    stop {
        signal SIGTERM
        timeout 30s
        expect exit_code == 0
        expect stderr contains "shutdown complete"
    }
}

Start a binary, wait for it to be healthy, run tests against it, then observe shutdown behavior. process compiles and runs a Go package by default. Use process run "./bin/server" to skip the build step for pre-built binaries.

Readiness modes: ready http "url", ready tcp "host:port", ready stdout "pattern", ready delay 5s.

The stop block is optional. Without it, the process is killed with SIGTERM after tests complete. With it, you control the signal, timeout, and can assert on exit code, stdout, and stderr during shutdown.

How it works

Danmuji extends Go's grammar using gotreesitter's grammargen package. The extended grammar parses .dmj files into a concrete syntax tree, then a transpiler walks the tree and emits Go code. The grammar adds ~50 new productions on top of Go's base grammar, all defined in pure Go using gotreesitter's composable DSL.

The generated code depends on:

These are dependencies of your test code, not of danmuji itself. Add them to your project with go get as needed.

File convention

myservice/
  user.go                  # implementation
  user_test.go             # existing Go tests (keep these)
  user_test.dmj            # danmuji specs (new)
  user_danmuji_test.go     # generated (gitignore or commit, your call)

Danmuji doesn't replace your existing tests. It layers on top.

Status

Working and tested. The suite covers grammar parsing, transpiler output, highlight queries, self-hosted .dmj meta specs, and end-to-end compile-and-run tests that verify the generated Go code actually compiles and executes correctly.

This is an early release. The grammar and transpiler are functional but the generated code patterns may evolve.

License

MIT

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	NewGrammar    = grammargen.NewGrammar
	ExtendGrammar = grammargen.ExtendGrammar
)

Constructor aliases

View Source
var (
	Str         = grammargen.Str
	Pat         = grammargen.Pat
	Sym         = grammargen.Sym
	Seq         = grammargen.Seq
	Choice      = grammargen.Choice
	Repeat      = grammargen.Repeat
	Repeat1     = grammargen.Repeat1
	Optional    = grammargen.Optional
	Token       = grammargen.Token
	ImmToken    = grammargen.ImmToken
	Field       = grammargen.Field
	Prec        = grammargen.Prec
	PrecLeft    = grammargen.PrecLeft
	PrecRight   = grammargen.PrecRight
	PrecDynamic = grammargen.PrecDynamic
	Alias       = grammargen.Alias
	Blank       = grammargen.Blank
	CommaSep    = grammargen.CommaSep
	CommaSep1   = grammargen.CommaSep1
)

DSL function aliases

View Source
var (
	AppendChoice            = grammargen.AppendChoice
	AddConflict             = grammargen.AddConflict
	Generate                = grammargen.Generate
	GenerateLanguage        = grammargen.GenerateLanguage
	GenerateLanguageAndBlob = grammargen.GenerateLanguageAndBlob
	LoadLanguageBlob        = gotreesitter.LoadLanguage
)

Helper function aliases

View Source
var (
	RuleBlank       = grammargen.RuleBlank
	RuleString      = grammargen.RuleString
	RulePattern     = grammargen.RulePattern
	RuleSymbol      = grammargen.RuleSymbol
	RuleSeq         = grammargen.RuleSeq
	RuleChoice      = grammargen.RuleChoice
	RuleRepeat      = grammargen.RuleRepeat
	RuleRepeat1     = grammargen.RuleRepeat1
	RuleOptional    = grammargen.RuleOptional
	RuleToken       = grammargen.RuleToken
	RuleImmToken    = grammargen.RuleImmToken
	RuleField       = grammargen.RuleField
	RulePrec        = grammargen.RulePrec
	RulePrecLeft    = grammargen.RulePrecLeft
	RulePrecRight   = grammargen.RulePrecRight
	RulePrecDynamic = grammargen.RulePrecDynamic
	RuleAlias       = grammargen.RuleAlias
)

Rule kind constants

View Source
var ExportGrammarJSON = grammargen.ExportGrammarJSON
View Source
var GenerateHighlightQueries = grammargen.GenerateHighlightQueries

GenerateHighlightQueries re-exports the highlight query generator.

View Source
var ImportGrammarJSON = grammargen.ImportGrammarJSON

Functions

func EmitGrammarGo

func EmitGrammarGo(g *Grammar, pkgName, funcName string) ([]byte, error)

EmitGrammarGo takes a Grammar IR and emits Go source code that reconstructs it using grammargen DSL calls. The output is a standalone Go file in the given package with a function of the given name that returns *Grammar.

func FormatDanmuji

func FormatDanmuji(source []byte) (string, error)

FormatDanmuji parses a .dmj source file and returns it with canonical indentation. Each nesting level uses one tab. Blank lines between top-level declarations are preserved.

func FormatParseError

func FormatParseError(source []byte, root *gotreesitter.Node, lang *gotreesitter.Language,
	sourceFile string, expectations map[string]*ProductionExpectations) string

FormatParseError walks the parse tree for ERROR/MISSING nodes and returns a human-readable multi-error string. Each error is formatted with source context, a caret underline, and (where possible) a hint showing correct syntax.

func TranspileDanmuji

func TranspileDanmuji(source []byte, opts TranspileOptions) (string, error)

TranspileDanmuji parses a .dmj source file and emits valid Go test code.

Types

type ErrorOverlay

type ErrorOverlay struct {
	Message string
	Example string
}

ErrorOverlay provides a curated message and example for a specific error context, keyed by "parent_node_type|prefix_signature".

type ExpectedStep

type ExpectedStep struct {
	Type     string
	Keyword  string
	Field    string
	Optional bool
}

ExpectedStep is one element in a LinearExpansion.

type Grammar

type Grammar = grammargen.Grammar

Type aliases

func DanmujiGrammar

func DanmujiGrammar() *Grammar

DanmujiGrammar returns a BDD testing DSL grammar that extends Go. It adds test blocks, BDD structure (given/when/then), assertions, test doubles (mock/fake/spy), lifecycle hooks, data tables, and tags.

func GoGrammar

func GoGrammar() *Grammar

GoGrammar returns the go grammar. Code generated by EmitGrammarGo. DO NOT EDIT.

type LinearExpansion

type LinearExpansion struct {
	Steps []ExpectedStep
}

LinearExpansion is a single linear path through a production.

type ProductionExpectations

type ProductionExpectations struct {
	NodeType   string
	Expansions []LinearExpansion
}

ProductionExpectations describes what a grammar production expects.

type Rule

type Rule = grammargen.Rule

type TranspileOptions

type TranspileOptions struct {
	SourceFile string
	Debug      bool
}

TranspileOptions controls optional transpiler behavior.

Directories

Path Synopsis
cmd
danmuji command
Package main implements the danmuji CLI tool that transpiles .dmj files to _danmuji_test.go files suitable for `go test`.
Package main implements the danmuji CLI tool that transpiles .dmj files to _danmuji_test.go files suitable for `go test`.
genblob command
Command genblob generates the embedded language blob and hash for the danmuji grammar.
Command genblob generates the embedded language blob and hash for the danmuji grammar.
Package register adds danmuji to the gotreesitter grammars registry so the .dmj extension is discoverable by registry consumers (editors, gts).
Package register adds danmuji to the gotreesitter grammars registry so the .dmj extension is discoverable by registry consumers (editors, gts).

Jump to

Keyboard shortcuts

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