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.