Documentation
¶
Overview ¶
Package sqltest is sqlkit's toolkit for testing SQL code. It brings together two complementary halves: helpers that seed and manage a real database, and a spy-and-mock pair built on querytrace for asserting on the queries code issues. Import the one package whether a test needs a seeded database, a query budget, or both.
Seeding a database ¶
The seeding helpers cut the boilerplate of managing the schema and inserting fixtures, leaving the connection (and any test container) to the caller. Every helper takes *testing.T and fails the test with t.Fatalf on error, so callers write linear setup code with no error handling.
A DB is the entry point. New wraps a *sqlkit.Database the caller opened against a disposable test database, returning a handle whose methods reset, isolate, and seed it. New itself changes nothing; an explicit SetupSchema applies the schema and drops it again on cleanup:
tdb := sqltest.New(database, md)
tdb.SetupSchema(t) // schema applied now, dropped on t.Cleanup
id := tdb.InsertID(t, appdb.Users, UserDTO{Email: "a@example.com"})
tdb.InsertRows(t, appdb.Posts, PostDTO{UserID: id, Title: "hello"})
Constructing a DB is the safety bar: the destructive operations (applying or dropping the schema, truncating tables) live only as DB methods, so a database the test did not deliberately wrap as throwaway is never dropped.
- Schema lifecycle: DB.SetupSchema (apply now, drop on t.Cleanup), and the methods DB.ApplySchema (apply, no teardown) and DB.Truncate for resetting within a test. Their drop and truncate steps are foreign-key-safe regardless of how the tables were declared — children before or after parents, cross-schema, or in a cycle.
- Isolation: DB.Tx hands each case its own transaction, rolled back on t.Cleanup, so cases that share one schema stay independent and may run in parallel. Snapshot (and Fixture.LoadSnapshot) instead capture the seeded state of a single session as a SAVEPOINT and return a restore function, so many serial cases reuse one seed by rolling back between them.
- Data insertion: the DB.InsertRows / DB.InsertID / DB.Load methods seed through auto-commit; the InsertRows, InsertID, InsertReturning, and Fixture.Load functions take any sqlkit.Querier, so they seed into a Tx session (isolated, rolled back with the case) just as well.
- Fixtures: Fixture, a named, ordered set of rows across tables, with Load.
The insert helpers smooth over the main cross-dialect wrinkle in seeding: reading back a database-generated key. InsertID uses RETURNING on PostgreSQL and Result.LastInsertId on MySQL, so fixture code that wires up foreign keys works on both without branching. Foreign-key ordering of the rows is the caller's responsibility: insert parent rows before the children that reference them, exactly as in hand-written seed code.
Asserting on queries ¶
The spy turns a querytrace.Trace into a record of the SQL one operation actually issued, and asserts on it after the fact. Where a mock library makes you declare the queries up front and fails if they do not run, the spy 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)
sqltest.Expect(t, trace).
MaxQueries(5).
NoWrites().
NoNPlusOne().
Query(sqltest.Select(), sqltest.Table("users")).Times(1)
Match values (Select, Table, Fingerprint, SqlContains, Caller, HasTopLevelWhere, …) 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.
Mocking a database ¶
For unit tests that should not touch a database at all, NewDB returns a *sql.DB backed by a programmable Mock and traced by querytrace, so the mock provides the inputs (canned rows, results, or errors registered with ExpectQuery and ExpectExec) and the spy assertions verify the calls.
Index ¶
- func AnyColumn() sqlpkg.Selection
- func AnyExpr() sqlpkg.Expression
- func AnyTable() sqlpkg.Source
- func Count(trace *querytrace.Trace, ms ...Match) int
- func Events(trace *querytrace.Trace, ms ...Match) []querytrace.Event
- func InsertID(t *testing.T, ex sqlkit.Querier, table sqlkit.Table, row any) int64
- func InsertReturning[Row any](t *testing.T, ex sqlkit.Querier, table sqlkit.Table, rows ...Row) []Row
- func InsertRows(t *testing.T, ex sqlkit.Querier, table sqlkit.Table, rows ...any)
- func Snapshot(t *testing.T, s *sqlkit.Session) (restore func(t *testing.T))
- type ArgMatcher
- type Assertion
- func (a *Assertion) AllRowsClosed() *Assertion
- func (a *Assertion) Committed() *Assertion
- func (a *Assertion) InOrder(ms ...Match) *Assertion
- func (a *Assertion) MaxQueries(n int) *Assertion
- func (a *Assertion) MinQueries(n int) *Assertion
- func (a *Assertion) NoNPlusOne() *Assertion
- func (a *Assertion) NoQueries() *Assertion
- func (a *Assertion) NoTransaction() *Assertion
- func (a *Assertion) NoWarn(code querytrace.WarningCode) *Assertion
- func (a *Assertion) NoWarnings() *Assertion
- func (a *Assertion) NoWrites() *Assertion
- func (a *Assertion) Queries(n int) *Assertion
- func (a *Assertion) Query(ms ...Match) *QueryAssertion
- func (a *Assertion) Reads(n int) *Assertion
- func (a *Assertion) RolledBack() *Assertion
- func (a *Assertion) Transactions(n int) *Assertion
- func (a *Assertion) UniqueQueries(n int) *Assertion
- func (a *Assertion) Warns(code querytrace.WarningCode) *Assertion
- func (a *Assertion) Writes(n int) *Assertion
- type DB
- func (d *DB) ApplySchema(t *testing.T)
- func (d *DB) Database() *sqlkit.Database
- func (d *DB) InsertID(t *testing.T, table sqlkit.Table, row any) int64
- func (d *DB) InsertRows(t *testing.T, table sqlkit.Table, rows ...any)
- func (d *DB) Load(t *testing.T, f *Fixture)
- func (d *DB) SetupSchema(t *testing.T)
- func (d *DB) Truncate(t *testing.T, tables ...sqlkit.Table)
- func (d *DB) Tx(t *testing.T, opts ...sqlkit.SessionOption) *sqlkit.Session
- type Fixture
- type Match
- func All(ms ...Match) Match
- func Any(ms ...Match) Match
- func Begin() Match
- func Caller(sub string) Match
- func Commit() Match
- func Ddl() Match
- func Delete() Match
- func Fingerprint(fp string) Match
- func HasTopLevelJoin() Match
- func HasTopLevelWhere() Match
- func Insert() Match
- func Label(key, value string) Match
- func Named(name string) Match
- func Not(m Match) Match
- func Op(k querytrace.StmtKind) Match
- func Params(n int) Match
- func ParamsAtLeast(n int) Match
- func Read() Match
- func Rollback() Match
- func Select() Match
- func Sql(text string) Match
- func SqlContains(sub string) Match
- func SqlMatches(re string) Match
- func Table(name string) Match
- func Update() Match
- func Write() Match
- type Matcher
- type Mock
- type Option
- type QueryAssertion
- type RowSet
- type Stub
- func (s *Stub) AnyTimes() *Stub
- func (s *Stub) MaxTimes(n int) *Stub
- func (s *Stub) MinTimes(n int) *Stub
- func (s *Stub) Required() *Stub
- func (s *Stub) Return(rows *RowSet) *Stub
- func (s *Stub) ReturnError(err error) *Stub
- func (s *Stub) ReturnResult(lastInsertID, rowsAffected int64) *Stub
- func (s *Stub) Times(n int) *Stub
- func (s *Stub) WillDelayFor(d time.Duration) *Stub
- func (s *Stub) WithArgs(args ...any) *Stub
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func AnyColumn ¶
AnyColumn is a Select selection that matches any column or expression:
mock.ExpectQuery(sql.Select(sqltest.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(sqltest.AnyExpr())
func AnyTable ¶
AnyTable is a From source that matches any table in a sql.Query expectation:
mock.ExpectQuery(sql.Select(sql.Col("id")).From(sqltest.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.
func InsertID ¶
InsertID inserts a single row and returns its database-generated primary key, smoothing over the dialect difference: on dialects with RETURNING (PostgreSQL) it reads the key back with RETURNING <pk>; otherwise (MySQL) it falls back to Result.LastInsertId. It assumes a single auto-generated integer primary-key column, which is the common test fixture; tables with a composite or non-integer key are not supported.
func InsertReturning ¶
func InsertReturning[Row any](t *testing.T, ex sqlkit.Querier, table sqlkit.Table, rows ...Row) []Row
InsertReturning inserts rows and scans the inserted rows — with database-generated columns filled in — back into a slice of Row. It relies on RETURNING, so it is PostgreSQL-only; on a dialect without RETURNING (MySQL) it skips the test rather than silently returning unpopulated rows. Use InsertRows or InsertID on those dialects.
func InsertRows ¶
InsertRows inserts rows into table and fails the test on error. Each row may be a struct, a pointer to a struct, or a slice of either — the same shapes InsertQuery.Values accepts — so generated row structs and caller-defined DTOs both work. It does not read anything back; use InsertID when you need a generated key, or InsertReturning to scan the inserted rows.
ex is any sqlkit.Querier, satisfied by both *sqlkit.Database (auto-commit) and *sqlkit.Session (inside a transaction).
func Snapshot ¶
Snapshot records the session's current state as a reusable restore point and returns a function that rolls back to it. Seed your fixtures into the session once, snapshot, then run each case against the same session and call the returned restore between cases to discard that case's writes and return to the seeded state — far cheaper than reloading the fixtures every time:
tdb := sqltest.New(db, md)
tdb.SetupSchema(t)
session := tdb.Tx(t) // one session, rolled back at the end
blog.Load(t, session) // seed once
restore := sqltest.Snapshot(t, session)
t.Run("case1", func(t *testing.T) {
defer restore(t) // undo case1's writes
// ... mutate and assert against session ...
})
t.Run("case2", func(t *testing.T) {
defer restore(t) // case2 sees the seeded state again
// ...
})
It uses a SAVEPOINT (Session.Savepoint / RollbackTo), so it works on PostgreSQL and MySQL alike and the restore point survives repeated restores. The catch is that every case must run its statements through the same session and the session must stay open and uncommitted: committing or closing it ends the transaction and invalidates the restore point. Cases therefore cannot run in parallel against the snapshot.
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"), sqltest.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 ¶
AllRowsClosed asserts no read left its row cursor open (no rows_not_closed warning).
func (*Assertion) InOrder ¶
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 ¶
MaxQueries asserts the trace recorded at most n statements, a query budget.
func (*Assertion) MinQueries ¶
MinQueries asserts the trace recorded at least n statements.
func (*Assertion) NoNPlusOne ¶
NoNPlusOne asserts the trace produced neither a possible-N+1 nor a query-in-loop warning.
func (*Assertion) NoQueries ¶
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 ¶
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 ¶
NoWarnings asserts the trace produced no warning at all.
func (*Assertion) NoWrites ¶
NoWrites asserts the trace recorded no write statement, the check for a read-only code path.
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) RolledBack ¶
RolledBack asserts at least one transaction rolled back.
func (*Assertion) Transactions ¶
Transactions asserts exactly n transactions began within the trace.
func (*Assertion) UniqueQueries ¶
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.
type DB ¶
type DB struct {
// contains filtered or unexported fields
}
DB is a throwaway test database: a thin handle over a *sqlkit.Database that owns the destructive test operations — applying and dropping the schema, truncating tables, and opening isolated per-test transactions — alongside auto-commit seeding sugar.
Constructing a DB is itself the safety bar. The drop and truncate operations live only as methods, reachable only through a DB a test deliberately built over a disposable database, so a *sqlkit.Database wired to a staging or production DSN — a stray environment variable, the wrong config profile — can never be wiped by a test that did not wrap it here. Construction touches nothing: SetupSchema and ApplySchema are the explicit steps that change the database, never New.
Do not confuse DB with NewDB: NewDB builds a programmable mock *sql.DB for unit tests that must not touch a database at all, whereas a DB drives a real one. The two halves of the package are independent.
func New ¶
New wraps db as a throwaway test database. It only builds the handle — it issues no SQL and changes nothing — so the schema is applied by an explicit SetupSchema (or ApplySchema), never as a side effect of construction:
tdb := sqltest.New(database, md)
tdb.SetupSchema(t) // drop + create now, dropped again on t.Cleanup
id := tdb.InsertID(t, appdb.Users, UserDTO{Email: "a@example.com"})
tdb.InsertRows(t, appdb.Posts, PostDTO{UserID: id, Title: "hello"})
md is the meta.Metadata a schema catalog produces (decl.Catalog.Build). db is a *sqlkit.Database the caller opened against a disposable test database — a container, a CI service database, or a scratch schema — never production.
func (*DB) ApplySchema ¶
ApplySchema applies md's schema — a foreign-key-safe drop of the existing tables followed by the CREATE DDL — registering no teardown. It is SetupSchema without the automatic drop on cleanup: reach for it to reset the schema again within a test, or when something else owns the teardown.
func (*DB) Database ¶
Database returns the underlying *sqlkit.Database, for running queries, seeding through a sqlkit.Querier helper (InsertRows, Fixture.Load, …), or any sqlkit operation the DB does not wrap.
func (*DB) InsertID ¶
InsertID inserts a single row through auto-commit and returns its database-generated primary key. It is shorthand for InsertID(t, d.Database(), table, row); see that function for the cross-dialect key read-back.
func (*DB) InsertRows ¶
InsertRows seeds rows into table through auto-commit (the underlying database), failing the test on error. It is shorthand for InsertRows(t, d.Database(), table, rows...); seed into a Tx session instead when cases run in parallel, so the rows roll back with the case rather than persisting.
func (*DB) Load ¶
Load seeds a fixture through auto-commit. It is shorthand for f.Load(t, d.Database()); load into a Tx session instead when cases run in parallel.
func (*DB) SetupSchema ¶
SetupSchema gives the test a clean schema and tears it down automatically: it applies md's schema now — a foreign-key-safe drop of any existing tables followed by the CREATE DDL md renders for the dialect — then registers a t.Cleanup that drops the tables again, leaving the database empty. Call it once at the top of a test, or of a parent test whose subtests share the schema.
func (*DB) Truncate ¶
Truncate deletes every row from the given tables foreign-key-safely, whatever order they were declared in. Called with no tables it clears every table in the schema. It is a faster between-cases reset than re-applying the schema when the tables already exist and only their data needs clearing.
func (*DB) Tx ¶
Tx gives the test its own transaction and rolls it back automatically when the test finishes. Each call opens a separate sqlkit.Session — a transaction on its own pooled connection — so cases that share the one schema stay isolated from each other's writes and can run in parallel: nothing a case inserts, updates, or deletes is visible to another, and the rollback leaves the database exactly as the next case (or the next run) finds it.
Apply the schema once for the group, then hand each case its own transaction:
func TestBlog(t *testing.T) {
tdb := sqltest.New(database, md)
tdb.SetupSchema(t) // schema created once, dropped after all subtests
t.Run("publish", func(t *testing.T) {
t.Parallel()
s := tdb.Tx(t) // isolated transaction, rolled back on return
blog.Load(t, s) // seed inside the transaction
// ... mutate and assert against s ...
})
t.Run("archive", func(t *testing.T) {
t.Parallel()
s := tdb.Tx(t) // sees none of "publish"'s writes
// ...
})
}
SetupSchema's teardown runs on the parent test's cleanup, which Go schedules after every parallel subtest has finished, so the shared schema outlives the cases.
The returned *sqlkit.Session is a sqlkit.Querier, so it drops into the seeding helpers (InsertRows, InsertID, Fixture.Load, …) and starts query chains (s.Select()…). The transaction begins lazily on the first statement, so a case that runs nothing costs nothing. Pass sqlkit.SessionOption values (WithTxOptions, …) to control isolation level or read-only intent.
Tx isolates through a real transaction per connection, which scales to parallel cases; Snapshot isolates through savepoints on one shared, serial session. Reach for Tx when cases run in parallel or each wants a clean slate, and for Snapshot when many serial cases reuse one expensive seed.
type Fixture ¶
type Fixture struct {
// contains filtered or unexported fields
}
Fixture is a named, ordered set of rows spanning one or more tables. Declare it once and Load it into a database in any test that needs the same starting data:
var blog = sqltest.NewFixture("blog").
Add(blogdb.Users, userRow{Email: "alice@example.com"}, userRow{Email: "bob@example.com"}).
Add(blogdb.Posts, postRow{Title: "Hello"})
blog.Load(t, db)
Groups load in declaration order, so add parent tables before the children that reference them — foreign-key ordering is the caller's responsibility, the same as hand-written seed code. Fixture is for static rows whose values are known up front; when later rows must reference a generated key (e.g. a post needs its author's id), capture it with InsertID in a plain seed function instead.
func NewFixture ¶
NewFixture creates an empty fixture. name appears in failure messages.
func (*Fixture) Add ¶
Add appends a group of rows for table, preserving call order. It returns the fixture so calls chain. Each row follows the same shapes InsertRows accepts (struct, pointer to struct, or slice of either).
func (*Fixture) Load ¶
Load inserts every group in declaration order, failing the test on the first error. ex is any sqlkit.Querier, so a fixture loads through a *sqlkit.Database or within a *sqlkit.Session transaction alike.
func (*Fixture) LoadSnapshot ¶
LoadSnapshot loads the fixture into the session and snapshots immediately, returning the restore function. It is Fixture.Load followed by Snapshot, for the common case of seeding a fixture and reusing it across cases.
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, HasTopLevelWhere, 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, HasTopLevelWhere, Fingerprint) need a parser wired into the Config; without one they do not match, while operation and SQL-text matchers still do.
func Caller ¶
Caller matches a statement issued from application code whose function name or file path contains sub. It needs caller capture (WithCaller).
func Fingerprint ¶
Fingerprint matches a statement with the given normalized fingerprint.
func HasTopLevelJoin ¶
func HasTopLevelJoin() Match
HasTopLevelJoin matches a statement whose top-level node carries a JOIN clause (from analysis). A JOIN inside a subquery, CTE body, or UNION arm does not count.
func HasTopLevelWhere ¶
func HasTopLevelWhere() Match
HasTopLevelWhere matches a statement whose top-level node carries a WHERE clause (from analysis). A WHERE inside a subquery, CTE body, or UNION arm does not count.
func ParamsAtLeast ¶
ParamsAtLeast matches a statement carrying at least n bind arguments, the signature of a large IN list or batch.
func Sql ¶
Sql matches a statement whose raw SQL equals text, ignoring surrounding whitespace. It needs raw SQL capture (WithRawSQL).
func SqlContains ¶
SqlContains matches a statement whose raw SQL contains sub, case-insensitively. It needs raw SQL capture (WithRawSQL).
func SqlMatches ¶
SqlMatches matches a statement whose raw SQL matches the regular expression re. It panics if re does not compile. It needs raw SQL capture (WithRawSQL).
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.
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 ¶
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 := sqltest.NewDB(t)
mock.ExpectQuery(sqltest.Contains("FROM users")).
Return(sqltest.NewRows("id", "name").AddRow(1, "alice"))
trace := querytrace.New("LoadUser")
ctx := querytrace.WithTrace(context.Background(), trace)
// ... use db under ctx ...
sqltest.Expect(t, trace).Query(sqltest.Select(), sqltest.Table("users")).Times(1)
func (*Mock) ExpectExec ¶
ExpectExec registers a response for ExecContext statements (db.Exec). See ExpectQuery for how the expectation is matched.
func (*Mock) ExpectQuery ¶
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).
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 NewConfig(WithVerboseCapture()): the PostgreSQL dialect, no parser, raw SQL and callers captured so every matcher works; supply your own (for example with a parser wired in) to enable the structural matchers Table, HasTopLevelWhere, 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.
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 ¶
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 ¶
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 ¶
MinTimes sets the fewest matching statements Verify requires. Zero (the default) requires none.
func (*Stub) Required ¶
Required marks the stub as one Verify must see matched at least once. It is shorthand for MinTimes(1).
func (*Stub) ReturnError ¶
ReturnError makes the stub answer with err.
func (*Stub) ReturnResult ¶
ReturnResult sets the result an exec stub answers with.
func (*Stub) Times ¶
Times sets the exact number of statements the stub answers and that Verify requires: shorthand for MinTimes(n) and MaxTimes(n).
func (*Stub) WillDelayFor ¶
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.