Testing your code

Table of contents
  1. Testing your code
    1. Introduction
    2. Creating a test database
    3. Migrating and seeding
      1. Testing with the whole pipeline
    4. Test helpers

Introduction

migro makes it simple to test code that touches the database: a throwaway database, migrations, and factories that build the data. This page is the equivalent of Laravel’s Database Testing.

Creating a test database

SQLite makes a fast test database. A file in t.TempDir() avoids the “each connection gets its own :memory: database” trap:

func newTestDB(t *testing.T) *migro.DB {
    db, err := migro.Open(migro.Config{Dialect: "sqlite", DSN: filepath.Join(t.TempDir(), "test.db")})
    if err != nil { t.Fatal(err) }
    t.Cleanup(func() { _ = db.Close() })
    return db
}

Migrating and seeding

Run your migrations in the test with migrate.New(db) and use factories to build data. Because dialect SQL differs, also run the suite against your production database type in CI.

Testing with the whole pipeline

To test against a realistic schema, register your migrations and seeders by importing their packages, then migrate and seed in the test:

import (
    _ "example.com/shop/database/migrations"   // init() registers each migration
    _ "example.com/shop/database/seeders"      // init() registers each seeder
)

func seededDB(t *testing.T) *migro.DB {
    db := newTestDB(t)
    m := migrate.New(db)
    if err := m.AddRegistered(); err != nil { t.Fatal(err) }
    if _, err := m.Latest(ctx); err != nil { t.Fatal(err) }

    r := seed.NewRunner(db, seed.WithEnvironment("testing"))
    if err := r.AddRegistered(); err != nil { t.Fatal(err) }
    if _, err := r.Seed(ctx); err != nil { t.Fatal(err) }   // DatabaseSeeder, or every seeder by name
    return db
}

Two traps:

  • SQLite does not enforce foreign keys by default. Without it, tests that expect a constraint error pass vacuously. Open the file with the pragma in the DSN: Dialect: "sqlite", DSN: path + "?_pragma=foreign_keys(1)".
  • Registered seeders run in name order when there is no DatabaseSeeder, which ignores foreign keys. Start from the root seeder (r.Seed(ctx)) or pick one with r.RunOnly(ctx, "UserSeeder").

The repository’s own integration/ suite is a worked example: 16 migrations, class seeders, factories and models, with tests for constraints, referential actions, every relation kind, rollbacks and seeder atomicity. It runs on SQLite, PostgreSQL and MySQL.

Test helpers

The dbtest package has the assertions and fixtures Laravel’s test case provides:

import "github.com/LouisRiverstone/migro/dbtest"

dbtest.AssertDatabaseHas(t, db, "users", map[string]any{"email": "ana@example.com"})
dbtest.AssertDatabaseMissing(t, db, "users", map[string]any{"email": "gone@example.com"})
dbtest.AssertDatabaseCount(t, db, "users", 3)
dbtest.AssertSoftDeleted(t, db, "users", map[string]any{"id": 7})     // deleted_at is set
dbtest.AssertNotSoftDeleted(t, db, "users", map[string]any{"id": 8})
dbtest.AssertQueryCount(t, db, 2, func() { /* code under test */ })   // catches N+1

dbtest.RefreshDatabase(t, db, migrations)   // drop everything, migrate from scratch
tx := dbtest.Transaction(t, db)             // rolled back when the test ends; use tx in the test

A nil value in the where map matches NULL. Transaction cannot roll back DDL that commits implicitly (MySQL).