Persistence
Give a factory a handler and Create builds and saves in one step.
The interface
Section titled “The interface”type PersistenceHandler[T any] interface { Save(ctx context.Context, instance T) (T, error) SaveMany(ctx context.Context, instances []T) ([]T, error)}Both methods return the persisted values, so a store that assigns IDs or timestamps can hand back what it actually wrote.
Wiring it up
Section titled “Wiring it up”type userStore struct{ db *sql.DB }
func (s userStore) Save(ctx context.Context, user User) (User, error) { row := s.db.QueryRowContext(ctx, "INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id", user.Name, user.Email, ) if err := row.Scan(&user.ID); err != nil { return User{}, err } return user, nil}
func (s userStore) SaveMany(ctx context.Context, users []User) ([]User, error) { saved := make([]User, 0, len(users)) for _, user := range users { created, err := s.Save(ctx, user) if err != nil { return nil, err } saved = append(saved, created) } return saved, nil}
factory := fabricator.New(User{}, fabricator.WithPersistenceHandler[User](userStore{db: db}),)Creating
Section titled “Creating”user := factory.Create(ctx)users := factory.CreateBatch(ctx, 10)
user, err := factory.CreateE(ctx)users, err := factory.CreateBatchE(ctx, 10)Create takes the same build options as Build:
admin := factory.Create(ctx, fabricator.Override(role, "admin"))CreateBatch builds the whole batch first, then calls SaveMany once, so a
handler can use a single statement or a transaction.
Errors
Section titled “Errors”Calling Create or CreateBatch on a factory with no handler fails:
cannot call .Create on a factory without a persistence handlerA handler’s own error is wrapped with the stage that failed:
persistence save failed: connection refusedpersistence save many failed: connection refusedThe context you pass is handed to the handler untouched, so cancellation and deadlines behave the way the store expects.
Cleanup
Section titled “Cleanup”Fabricator does not track what it created and has no teardown of its own. Use
whatever the test already uses — a transaction rolled back in t.Cleanup, a
truncate, or a disposable schema:
func TestSomething(t *testing.T) { tx, err := db.BeginTx(t.Context(), nil) require.NoError(t, err) t.Cleanup(func() { _ = tx.Rollback() })
factory := fabricator.New(User{}, fabricator.WithPersistenceHandler[User](userStore{db: tx}), ) user := factory.Create(t.Context()) // ...}