Package loading
Package loading groups multiple service registrations.
Registration
The services can be assembled into a package, and then, exported all at once.
- pkg/stores/package.go
- pkg/observability/package.go
- cmd/main.go
package stores
var Package = do.Package(
do.Lazy(NewPostgreSQLConnectionService),
do.Lazy(NewUserRepository),
do.Lazy(NewArticleRepository),
)
package observability
var Package = do.Package(
do.Eager(slog.New(slog.NewTextHandler(os.Stdout, nil))),
do.EagerNamed("prometheus.collector", DefaultMetricCollector),
)
package main
import (
"github.com/foo/bar/pkg/stores"
"github.com/foo/bar/pkg/observability"
"github.com/foo/bar/pkg/handlers"
)
func main() {
injector := do.New()
stores.Package(injector)
observability.Package(injector)
// could be replaced by:
// injector := do.New(
// stores.Package,
// observability.Package,
// )
// optional scope:
scope := injector.Scope("handlers", handlers.Package)
}
Play: https://go.dev/play/p/kmf8aOVyj96
The traditional vocab can be translated for package registration:
Provide[T](Injector, Provider[T])->Lazy(Provider[T])ProvideNamed[T](Injector, string, Provider[T])->LazyNamed(string, Provider[T])ProvideValue(Injector, T)->Eager(T)ProvideNamedValue[T](Injector, string, T)->EagerNamed(string, T)ProvideTransient[T](Injector, Provider[T])->Transient(Provider[T])ProvideNamedTransient[T](Injector, string, Provider[T])->TransientNamed(string, Provider[T])As[Initial, Alias](Injector)->Bind[Initial, Alias]()AsNamed[Initial, Alias](Injector, string, string)->BindNamed[Initial, Alias](string, string)
Testing and mocking
A package can ship a second variant, exposing test doubles behind the same interfaces as the production services. Swapping Package for its mock counterpart in a test injector replaces every service it registers, without touching the code under test.
This requires each service to be exposed through an interface (see Accept interfaces, return structs), with both the real and the mock implementation satisfying it.
- pkg/repositories/user_repository.go
- pkg/repositories/user_repository_mock.go
- pkg/repositories/package.go
- cmd/main.go
- pkg/auth/auth_test.go
package repositories
type UserRepository interface {
FindByID(id string) (*User, error)
}
func newUserRepository(i do.Injector) (UserRepository, error) {
pool := do.MustInvoke[*pgxpool.Pool](i)
return &userRepository{pool: pool}, nil
}
type userRepository struct {
pool *pgxpool.Pool
}
func (r *userRepository) FindByID(id string) (*User, error) {
// query the database
}
var _ UserRepository = (*userRepository)(nil)
package repositories
func newUserRepositoryMock(i do.Injector) (UserRepository, error) {
return &userRepositoryMock{users: map[string]*User{}}, nil
}
type userRepositoryMock struct {
users map[string]*User
}
func (r *userRepositoryMock) FindByID(id string) (*User, error) {
// return a fixture, or an error if absent
}
var _ UserRepository = (*userRepositoryMock)(nil)
package repositories
var Package = do.Package(
do.Lazy(newPostgreSQLPool),
do.Lazy(newUserRepository),
do.Lazy(newBillingRepository),
)
var PackageMock = do.Package(
do.Lazy(newUserRepositoryMock),
do.Lazy(newBillingRepositoryMock),
)
package main
import (
"github.com/foo/bar/pkg/auth"
"github.com/foo/bar/pkg/repositories"
)
func main() {
injector := do.New(
auth.Package,
repositories.Package,
)
}
package auth
import (
"testing"
"github.com/foo/bar/pkg/repositories"
)
func TestAuthRegister(t *testing.T) {
injector := do.New(
Package,
repositories.PackageMock,
)
// ...
}
Play: https://go.dev/play/p/s-ZWLUGiMaT
Only mock the packages relevant to the test. Combining repositories.Package for services you don't need to fake with repositories.PackageMock for the one you do is a common pattern, as long as both variants don't register the same service twice in the same injector.