Testing
LiteORM’s CGo-free SQLite makes tests cheap: an in-memory database opens in microseconds, needs no external service, and cross-compiles anywhere go test runs. This guide covers the patterns that make a LiteORM test suite fast and isolated — one database per test, rollback-based isolation, seeding fixtures, and asserting on the normalized error sentinels so a test written against SQLite still describes real behavior.
In-memory SQLite
Section titled “In-memory SQLite”Open an in-memory database with the literal path :memory: — no file, nothing to clean up, torn down when you close it. Migrate your models with orm.AutoMigrate, run the code under test, and close:
func newTestDB(t *testing.T) *liteorm.DB { t.Helper() db, err := sqlite.Open(":memory:") if err != nil { t.Fatal(err) } t.Cleanup(func() { db.Close() }) if err := orm.AutoMigrateAll(context.Background(), db, User{}, Post{}); err != nil { t.Fatal(err) } return db}sqlite.Open applies the production pragma preset (WAL, a busy timeout, foreign keys) just as it does in production, so your tests exercise the same settings your app runs under. Registering the cleanup with t.Cleanup means every test that calls this helper closes its database automatically.
One database per test
Section titled “One database per test”The cleanest isolation is a fresh in-memory database per test — no shared state to leak between tests, and they parallelize freely. A :memory: database is private to its handle, so two tests that each call newTestDB get two completely independent databases; nothing one writes is visible to the other.
func TestCreateUser(t *testing.T) { t.Parallel() db := newTestDB(t) ctx := context.Background()
users := orm.NewRepo[User](db) if err := users.Create(ctx, &User{Email: "ada@example.com"}); err != nil { t.Fatal(err) }
got, err := users.Get(ctx, 1) if err != nil { t.Fatal(err) } if got.Email != "ada@example.com" { t.Fatalf("got %q", got.Email) }}Because opening :memory: and running AutoMigrate is so cheap, per-test databases are the default recommendation — reach for the rollback pattern below only when migrating a large schema per test becomes a measurable cost.
Rollback-based isolation
Section titled “Rollback-based isolation”When you’d rather migrate once and share the schema, give each test a transaction and roll it back at the end. Every builder and repository accepts a *liteorm.BoundTx in place of the *liteorm.DB (both satisfy liteorm.Session), so the code under test runs unchanged inside the transaction, and the deferred rollback discards everything it wrote:
func withTx(t *testing.T, db *liteorm.DB, fn func(tx *liteorm.BoundTx)) { t.Helper() ctx := context.Background() tx, err := db.Begin(ctx) if err != nil { t.Fatal(err) } defer tx.Rollback(ctx) // discard everything the test wrote fn(tx)}
func TestTransfer(t *testing.T) { db := sharedTestDB(t) // migrated once for the package withTx(t, db, func(tx *liteorm.BoundTx) { // run the code under test against tx; asserts here })}This keeps a single migrated database for the whole package and resets to a clean slate after each test by throwing the transaction away. Tests that share one database this way should not run in parallel against the same handle. See transactions for the full transaction and savepoint model.
Fixtures and seed data
Section titled “Fixtures and seed data”Seed a graph of related rows by running ordered steps in one transaction, resolving references as ordinary Go variables — a later step reads the primary keys an earlier step generated. Wrapping the steps in a single transaction makes the whole fixture set atomic: if any step fails, the database is never left half-seeded.
type seedStep func(ctx context.Context, sess liteorm.Session) error
// seed runs steps in order in one transaction; any failure rolls the whole set back.func seed(ctx context.Context, db *liteorm.DB, steps ...seedStep) error { tx, err := db.Begin(ctx) if err != nil { return err } for _, step := range steps { if err := step(ctx, tx); err != nil { _ = tx.Rollback(ctx) return err } } return tx.Commit(ctx)}Reference resolution is just Go: create the parent, then use its generated ID when creating children.
var acme Orgerr := seed(ctx, db, func(ctx context.Context, sess liteorm.Session) error { acme = Org{Name: "Acme"} return orm.NewRepo[Org](sess).Create(ctx, &acme) }, func(ctx context.Context, sess liteorm.Session) error { r := orm.NewRepo[Member](sess) for _, m := range []*Member{ {OrgID: acme.ID, Name: "Ada"}, // reads the id the first step generated {OrgID: acme.ID, Name: "Grace"}, } { if err := r.Create(ctx, m); err != nil { return err } } return nil },)There’s no separate fixtures package to learn — a fixture is plain Go that inserts rows through the same repositories your application uses. The runnable examples/fixtures program builds out this pattern end to end.
Assert on normalized errors
Section titled “Assert on normalized errors”Don’t match a driver’s error string — it differs per backend and is brittle. LiteORM normalizes constraint and not-found failures into sentinel errors you test with errors.Is, so a test asserting “this insert violates a unique constraint” describes the behavior portably:
func TestDuplicateEmailRejected(t *testing.T) { db := newTestDB(t) ctx := context.Background() users := orm.NewRepo[User](db)
_ = users.Create(ctx, &User{Email: "ada@example.com"}) err := users.Create(ctx, &User{Email: "ada@example.com"})
if !errors.Is(err, liteorm.ErrUniqueViolation) { t.Fatalf("want ErrUniqueViolation, got %v", err) }}The one-line classifiers read well in assertions too — liteorm.IsUniqueViolation(err), IsNotFound(err), IsForeignKeyViolation(err), and the rest. A single-row read that finds nothing returns liteorm.ErrNoRows (which is sql.ErrNoRows), so errors.Is(err, liteorm.ErrNoRows) is your not-found assertion. Because these sentinels are identical across backends, a suite you run against in-memory SQLite asserts the same conditions your Postgres or MySQL deployment will hit.
See also
Section titled “See also”- Transactions — the
BoundTxand savepoints behind rollback isolation. - Errors — the normalized sentinels and classifier helpers you assert on.
- Connecting to a database — opening SQLite, including
:memory:. examples/fixtures— the runnable seeding pattern.