Dec 29, 2024
Testfixtures for your database in Golang
3 min read
When writing integration tests that run against your database, fixtures are an extremely powerful way to populate the database with sane defaults amongst others. As an example, you would not want to manually recreate and populate 10 different tables just to run one single test.
Take this as an example, you are building a SaaS with users and each user can belong to a workspace and items created by users can only be assigned to workspaces. So you have this dependency where to create an item, you have to create a user but to create a user, a workspace must be existing already.
The above scenario can get cumbersome and repetitive and can lead to tests not being written. When its hard to get tests going, it becomes hard for devs to write them.
What are fixtures
Fixtures are essentially YAML files you use to provide and describe the _shape of your data. This data and file will ultimately be processed by your test process and write that into your database.
Creating a fixture
I like to put them in the testdata/fixtures
path but you can put it anywhere
really. The next step is to take a look at your database table and map it into
yaml format. As an example, most SaaS products will include a plans
table that can be modeled
as
type Plan struct {
ID uuid.UUID
PlanName string
Reference string
Metadata PlanMetadata
DefaultPriceID string
Amount int64
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time
}
You can then map this into your YAML file as below:
---
- id: 4590c0bb-7dc1-4ec0-abef-61eb305059d1
reference: prod_QmtErtydaJZymT
plan_name: Core
default_price_id: price_1PvJSMIuzgc0GUapiNyXBEaH
amount: 4000
metadata: "{}"
created_at: "2024-09-04 13:46:35.612449+00"
updated_at: "2024-09-04 13:46:35.612449+00"
deleted_at: null
- id: 4ef53bbf-c660-4cf6-b737-6add758343df
reference: prod_QmtFLR9JvXLryD
plan_name: Scale
default_price_id: price_1PvJT8Iuzgc0GUapIRBpxCyJ
amount: 9900
metadata: "{}"
created_at: "2024-09-04 13:46:35.612449+00"
updated_at: "2024-09-04 13:46:35.612449+00"
deleted_at: null
In the example above, i have described and created 2 plans - Core and Scale.
Loading this into your database
package postgres
import (
"database/sql"
"fmt"
"testing"
testfixtures "github.com/go-testfixtures/testfixtures/v3"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database/postgres"
_ "github.com/golang-migrate/migrate/v4/source/file"
"github.com/stretchr/testify/require"
)
func prepareTestDatabase(t *testing.T, dsn string) {
t.Helper()
var err error
db, err := sql.Open("postgres", dsn)
require.NoError(t, err)
require.NoError(t, db.Ping())
driver, err := postgres.WithInstance(db, &postgres.Config{})
require.NoError(t, err)
// Run your migrations to create the tables first
migrator, err := migrate.NewWithDatabaseInstance(
fmt.Sprintf("file://%s", "migrations"), "postgres", driver)
require.NoError(t, err)
if err := migrator.Up(); err != nil && err != migrate.ErrNoChange {
t.Fatal(err)
}
fixtures, err := testfixtures.New(
testfixtures.Database(db),
testfixtures.Dialect("postgres"),
testfixtures.Directory("testdata/fixtures"),
)
require.NoError(t, err)
err = fixtures.Load()
require.NoError(t, err)
}
I like to run this as a setup on every test-case.