querytest

package
v0.0.0-...-de5c544 Latest Latest
Warning

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

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

Documentation

Overview

Package querytest turns a querytrace.Trace into a test spy: it records the SQL one operation actually issued, and this package asserts on it after the fact. Where a mock library makes you declare the queries up front and fails if they do not run, querytest works the other way around — run the code under a Trace, then ask what it did: a query budget, no writes, no N+1, a SELECT on a given table that ran exactly once, an INSERT before the COMMIT.

The entry point is Expect, a fluent assertion bound to a testing.TB:

trace := querytrace.New("GetUserPage")
ctx := querytrace.WithTrace(context.Background(), trace)
service.RenderUserPage(ctx, id)

querytest.Expect(t, trace).
	MaxQueries(5).
	NoWrites().
	NoNPlusOne().
	Query(querytest.Select(), querytest.Table("users")).Times(1)

Match values (Select, Table, Fingerprint, SqlContains, Caller, HasWhere, …) select the statements an assertion applies to; pass several and they must all hold. All, Any, and Not compose them. Count and Events expose the same matching without a testing.TB, for inspecting a Trace directly.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func AnyColumn

func AnyColumn() sqlpkg.Selection

AnyColumn is a Select selection that matches any column or expression:

mock.ExpectQuery(sql.Select(querytest.AnyColumn()).From(sql.Tbl("users")))

func AnyExpr

func AnyExpr() sqlpkg.Expression

AnyExpr is an expression that matches any expression, for Where and other clauses:

...Where(querytest.AnyExpr())

func AnyTable

func AnyTable() sqlpkg.Source

AnyTable is a From source that matches any table in a sql.Query expectation:

mock.ExpectQuery(sql.Select(sql.Col("id")).From(querytest.AnyTable()))

func Count

func Count(trace *querytrace.Trace, ms ...Match) int

Count returns how many statements in trace match every Match in ms. It is the spy query underneath Query(...).Times, usable without a testing.TB.

func Events

func Events(trace *querytrace.Trace, ms ...Match) []querytrace.Event

Events returns the statements in trace matching every Match in ms, in record order.

Types

type ArgMatcher

type ArgMatcher interface {
	// MatchArg reports whether the executed bind value matches.
	MatchArg(v any) bool
	// String describes the matcher.
	String() string
}

ArgMatcher decides whether the bind argument at a placeholder position matches. Place one in a sql.Query expectation with Arg, AnyArg, or ArgMatch — for example Where(Eq(Col("id"), querytest.Arg(5))) — and the mock checks the executed statement's argument there. Placeholders without an ArgMatcher (plain values) are left unconstrained, so a query matches any value unless you say otherwise.

func AnyArg

func AnyArg() ArgMatcher

AnyArg matches any bind argument. It is explicit documentation that a value is deliberately unconstrained.

func Arg

func Arg(v any) ArgMatcher

Arg matches a bind argument equal to v, comparing by database/sql driver value (so an int and the int64 the driver sees compare equal).

func ArgMatch

func ArgMatch(fn func(v any) bool) ArgMatcher

ArgMatch matches a bind argument for which fn returns true.

type Assertion

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

Assertion is a fluent set of checks against a single Trace. Each method reports a failure through the testing.TB and returns the Assertion so checks chain. Build one with Expect.

func Expect

func Expect(t testing.TB, trace *querytrace.Trace) *Assertion

Expect snapshots trace and returns an Assertion over it. Call it once the code under test has run; the snapshot is stable, so later checks see the same statements and warnings even if the Trace keeps recording.

func (*Assertion) AllRowsClosed

func (a *Assertion) AllRowsClosed() *Assertion

AllRowsClosed asserts no read left its row cursor open (no rows_not_closed warning).

func (*Assertion) Committed

func (a *Assertion) Committed() *Assertion

Committed asserts at least one transaction committed.

func (*Assertion) InOrder

func (a *Assertion) InOrder(ms ...Match) *Assertion

InOrder asserts that statements matching ms appear in the trace in the given relative order: an event matching ms[0] before one matching ms[1], and so on. The matched events need not be adjacent, and matchers may be transaction matchers (Commit, Rollback, Begin), so Insert() before Commit() is expressible. Compose multi-condition steps with All.

func (*Assertion) MaxQueries

func (a *Assertion) MaxQueries(n int) *Assertion

MaxQueries asserts the trace recorded at most n statements, a query budget.

func (*Assertion) MinQueries

func (a *Assertion) MinQueries(n int) *Assertion

MinQueries asserts the trace recorded at least n statements.

func (*Assertion) NoNPlusOne

func (a *Assertion) NoNPlusOne() *Assertion

NoNPlusOne asserts the trace produced neither a possible-N+1 nor a query-in-loop warning.

func (*Assertion) NoQueries

func (a *Assertion) NoQueries() *Assertion

NoQueries asserts the trace touched the database not at all, the check for a path that should be served without a query (a cache hit).

func (*Assertion) NoTransaction

func (a *Assertion) NoTransaction() *Assertion

NoTransaction asserts no transaction began within the trace.

func (*Assertion) NoWarn

func (a *Assertion) NoWarn(code querytrace.WarningCode) *Assertion

NoWarn asserts the trace produced no warning with code.

func (*Assertion) NoWarnings

func (a *Assertion) NoWarnings() *Assertion

NoWarnings asserts the trace produced no warning at all.

func (*Assertion) NoWrites

func (a *Assertion) NoWrites() *Assertion

NoWrites asserts the trace recorded no write statement, the check for a read-only code path.

func (*Assertion) Queries

func (a *Assertion) Queries(n int) *Assertion

Queries asserts the trace recorded exactly n read and write statements.

func (*Assertion) Query

func (a *Assertion) Query(ms ...Match) *QueryAssertion

Query narrows the assertion to the statements matching every Match in ms and returns a QueryAssertion to check how many ran. With no matchers it selects every statement.

func (*Assertion) Reads

func (a *Assertion) Reads(n int) *Assertion

Reads asserts the trace recorded exactly n read statements.

func (*Assertion) RolledBack

func (a *Assertion) RolledBack() *Assertion

RolledBack asserts at least one transaction rolled back.

func (*Assertion) Transactions

func (a *Assertion) Transactions(n int) *Assertion

Transactions asserts exactly n transactions began within the trace.

func (*Assertion) UniqueQueries

func (a *Assertion) UniqueQueries(n int) *Assertion

UniqueQueries asserts the trace recorded exactly n distinct query shapes (fingerprints).

func (*Assertion) Warns

func (a *Assertion) Warns(code querytrace.WarningCode) *Assertion

Warns asserts the trace produced a warning with code.

func (*Assertion) Writes

func (a *Assertion) Writes(n int) *Assertion

Writes asserts the trace recorded exactly n write statements.

type Match

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

Match is a predicate over a recorded Event. Matchers select the statements an assertion or a spy query applies to; pass several to Query, Count, or Events and they must all hold (logical AND). All, Any, and Not compose them explicitly.

Most matchers look at the statement: Select/Insert/Update/Delete, Table, Fingerprint, HasWhere, Params, SqlContains. A few look at the surrounding Event (Named, Caller, Label, InTransaction) or the transaction timeline (Begin, Commit, Rollback). Matchers that depend on structural analysis (Table, HasWhere, Fingerprint) need a parser wired into the Config; without one they do not match, while operation and SQL-text matchers still do.

func All

func All(ms ...Match) Match

All matches an Event only when every Match in ms does.

func Any

func Any(ms ...Match) Match

Any matches an Event when any Match in ms does.

func Begin

func Begin() Match

Begin matches a transaction begin on the timeline.

func Caller

func Caller(sub string) Match

Caller matches a statement issued from application code whose function name or file path contains sub. It needs Config.CaptureCaller.

func Commit

func Commit() Match

Commit matches a transaction commit on the timeline.

func Ddl

func Ddl() Match

Ddl matches a schema statement.

func Delete

func Delete() Match

Delete matches a DELETE.

func Fingerprint

func Fingerprint(fp string) Match

Fingerprint matches a statement with the given normalized fingerprint.

func HasJoin

func HasJoin() Match

HasJoin matches a statement that carries a JOIN clause (from analysis).

func HasWhere

func HasWhere() Match

HasWhere matches a statement that carries a WHERE clause (from analysis).

func Insert

func Insert() Match

Insert matches an INSERT.

func Label

func Label(key, value string) Match

Label matches a statement carrying the label key=value (WithLabel).

func Named

func Named(name string) Match

Named matches a statement tagged with the given query name (WithQueryName or Named).

func Not

func Not(m Match) Match

Not inverts a Match.

func Op

Op matches a statement of the given kind.

func Params

func Params(n int) Match

Params matches a statement carrying exactly n bind arguments.

func ParamsAtLeast

func ParamsAtLeast(n int) Match

ParamsAtLeast matches a statement carrying at least n bind arguments, the signature of a large IN list or batch.

func Read

func Read() Match

Read matches any read statement.

func Rollback

func Rollback() Match

Rollback matches a transaction rollback on the timeline.

func Select

func Select() Match

Select matches a SELECT (or other read).

func Sql

func Sql(text string) Match

Sql matches a statement whose raw SQL equals text, ignoring surrounding whitespace. It needs Config.CaptureRawSQL.

func SqlContains

func SqlContains(sub string) Match

SqlContains matches a statement whose raw SQL contains sub, case-insensitively. It needs Config.CaptureRawSQL.

func SqlMatches

func SqlMatches(re string) Match

SqlMatches matches a statement whose raw SQL matches the regular expression re. It panics if re does not compile. It needs Config.CaptureRawSQL.

func Table

func Table(name string) Match

Table matches a statement that references table, by name and case-insensitively. It needs structural analysis (a parser in the Config).

func Update

func Update() Match

Update matches an UPDATE.

func Write

func Write() Match

Write matches any write statement (INSERT/UPDATE/DELETE).

func (Match) String

func (m Match) String() string

String renders the matcher for failure messages.

type Matcher

type Matcher interface {
	// Matches reports whether a statement with the given SQL matches.
	Matches(sql string) bool
	// String describes the matcher for Verify failure messages.
	String() string
}

Matcher decides whether a mock stub answers a statement, by its SQL text. The built-in matchers are Contains (a partial match), Regexp, Equals, and Anything; ExpectQuery and ExpectExec also accept a plain string or a sql.Query for convenience. Implement Matcher for custom matching.

It is distinct from the spy-side Match, which selects recorded statements by their analyzed shape; a Matcher sees only the raw SQL, at the point the mock must answer it.

func Anything

func Anything() Matcher

Anything matches every statement.

func Contains

func Contains(sub string) Matcher

Contains matches a statement whose SQL contains sub, case-insensitively.

func Equals

func Equals(s string) Matcher

Equals matches a statement whose SQL equals s, ignoring case and runs of whitespace.

func Regexp

func Regexp(expr string) Matcher

Regexp matches a statement whose SQL matches the regular expression expr. It panics if expr does not compile.

type Mock

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

Mock is a programmable database/sql test double. It backs the *sql.DB returned by NewDB, answering each statement with a response you registered — canned rows, a result, or an error — so code that talks to a database can run in a unit test without one. Because the same DB is traced by querytrace, the spy assertions (Expect, Count, Events) see every statement the code issued, so the mock provides the inputs and the spy verifies the calls.

Register responses with ExpectQuery and ExpectExec. Each takes a Matcher that decides which statements the stub answers — Contains for a partial match, Regexp, or Equals — a plain string (matched with Equals), or a sql.Query built with the sqlkit builder. A sql.Query matches its compiled shape, and you may leave holes in it: AnyTable, AnyColumn, and AnyExpr match any table, column, or expression, while Arg, AnyArg, and ArgMatch constrain (or wildcard) a bind argument. Stubs are tried in registration order; the first applicable one answers. A statement matching no stub gets a lenient default — empty rows or a zero result — so a mock need only program the statements a test cares about. Verify reports any stub whose MinTimes (or Required) was not met.

func NewDB

func NewDB(t testing.TB, opts ...Option) (*sql.DB, *Mock)

NewDB returns a *sql.DB backed by a programmable Mock and traced by querytrace, so statements run under a context carrying a Trace are recorded for the spy assertions. It registers a t.Cleanup that verifies the mock and closes the DB when the test ends, so a missed Required stub — or, by default, any statement no stub answered — fails the test without a manual Verify call. Pass Lenient to allow unmatched statements.

db, mock := querytest.NewDB(t)
mock.ExpectQuery(querytest.Contains("FROM users")).
	Return(querytest.NewRows("id", "name").AddRow(1, "alice"))

trace := querytrace.New("LoadUser")
ctx := querytrace.WithTrace(context.Background(), trace)
// ... use db under ctx ...
querytest.Expect(t, trace).Query(querytest.Select(), querytest.Table("users")).Times(1)

func (*Mock) ExpectExec

func (m *Mock) ExpectExec(expect any) *Stub

ExpectExec registers a response for ExecContext statements (db.Exec). See ExpectQuery for how the expectation is matched.

func (*Mock) ExpectQuery

func (m *Mock) ExpectQuery(expect any) *Stub

ExpectQuery registers a response for QueryContext statements (db.Query, db.QueryRow) whose SQL the matcher accepts. The matcher is a Matcher (Contains, Regexp, Equals, Anything), a plain string (matched with Equals), or a sql.Query (compiled with the mock's dialect, then Equals).

func (*Mock) Verify

func (m *Mock) Verify(t testing.TB)

Verify reports any stub whose MinTimes (or Required) was not met, and — unless the mock is Lenient — any statement that matched no stub. NewDB registers it as a t.Cleanup, so calling it by hand is optional; it runs its checks only once.

type Option

type Option func(*config)

Option configures NewDB.

func Lenient

func Lenient() Option

Lenient turns off strict verification: a statement that matches no stub is allowed (it gets the empty-rows / zero-result default) instead of failing the test at cleanup. Use it for code paths that deliberately issue statements the mock does not program.

func WithConfig

func WithConfig(cfg querytrace.Config) Option

WithConfig sets the querytrace.Config the mock DB records with. The default is DebugConfig("postgres"), which captures raw SQL and callers so every matcher works; supply your own (for example with a parser wired in) to enable the structural matchers Table, HasWhere, and Fingerprint.

type QueryAssertion

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

QueryAssertion narrows an Assertion to the statements matching a set of matchers, ready to check how many of them ran. Build one with Assertion.Query; its terminal methods (Times, MinTimes, …) report the result and return the parent Assertion so checks keep chaining. The vocabulary mirrors the mock's Stub call-count methods.

func (*QueryAssertion) MaxTimes

func (q *QueryAssertion) MaxTimes(n int) *Assertion

MaxTimes asserts at most n matching statements ran.

func (*QueryAssertion) MinTimes

func (q *QueryAssertion) MinTimes(n int) *Assertion

MinTimes asserts at least n matching statements ran.

func (*QueryAssertion) Never

func (q *QueryAssertion) Never() *Assertion

Never asserts no matching statement ran. It is shorthand for Times(0).

func (*QueryAssertion) Times

func (q *QueryAssertion) Times(n int) *Assertion

Times asserts exactly n matching statements ran.

type RowSet

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

RowSet is a set of canned rows a query stub returns. Build it with NewRows and add rows with AddRow.

func NewRows

func NewRows(columns ...string) *RowSet

NewRows starts a RowSet with the given column names.

func (*RowSet) AddRow

func (r *RowSet) AddRow(values ...any) *RowSet

AddRow appends a row. The values are coerced to the database/sql driver value types, so plain ints, strings, and the like are accepted.

func (*RowSet) RowError

func (r *RowSet) RowError(row int, err error) *RowSet

RowError makes iteration fail with err when the cursor reaches row (0-based), for testing a read that breaks mid-stream — the rows lifecycle querytrace observes.

type Stub

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

Stub is one programmed response. Build it with ExpectQuery or ExpectExec and set its reply with Return, ReturnResult, or ReturnError. The setters chain.

func (*Stub) AnyTimes

func (s *Stub) AnyTimes() *Stub

AnyTimes lets the stub answer any number of statements, zero included, and imposes no Verify floor. It clears an earlier Times/MinTimes/MaxTimes/Required.

func (*Stub) MaxTimes

func (s *Stub) MaxTimes(n int) *Stub

MaxTimes caps how many statements the stub answers; further matching statements fall through to later stubs or the lenient default. Zero (the default) is unlimited.

func (*Stub) MinTimes

func (s *Stub) MinTimes(n int) *Stub

MinTimes sets the fewest matching statements Verify requires. Zero (the default) requires none.

func (*Stub) Required

func (s *Stub) Required() *Stub

Required marks the stub as one Verify must see matched at least once. It is shorthand for MinTimes(1).

func (*Stub) Return

func (s *Stub) Return(rows *RowSet) *Stub

Return sets the rows a query stub answers with.

func (*Stub) ReturnError

func (s *Stub) ReturnError(err error) *Stub

ReturnError makes the stub answer with err.

func (*Stub) ReturnResult

func (s *Stub) ReturnResult(lastInsertID, rowsAffected int64) *Stub

ReturnResult sets the result an exec stub answers with.

func (*Stub) Times

func (s *Stub) Times(n int) *Stub

Times sets the exact number of statements the stub answers and that Verify requires: shorthand for MinTimes(n) and MaxTimes(n).

func (*Stub) WillDelayFor

func (s *Stub) WillDelayFor(d time.Duration) *Stub

WillDelayFor makes the stub wait d before answering, returning the context's error if it is canceled or its deadline passes first. It exercises slow-query and cancellation handling — querytrace records a mid-iteration cancellation as a warning.

func (*Stub) WithArgs

func (s *Stub) WithArgs(args ...any) *Stub

WithArgs constrains the stub to statements whose bind arguments match, by position: an ArgMatcher (Arg, AnyArg, ArgMatch) is used as given, any other value becomes an exact Arg. It is the string-matcher counterpart to embedding Arg in a sql.Query.

Jump to

Keyboard shortcuts

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