[teamai] Push 87 resource(s) from XingfenD
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
# Advanced — google/wire
|
||||
|
||||
Detail topics referenced from `SKILL.md`. Each section is self-contained.
|
||||
|
||||
## Cleanup Chains
|
||||
|
||||
When a provider returns `(T, func(), error)`, Wire adds the cleanup to a chain. The generated injector runs cleanups in **reverse construction order**: the last-built dependant is cleaned up first, ensuring dependants are torn down before their dependencies.
|
||||
|
||||
```go
|
||||
// Provider with cleanup
|
||||
func NewDB(cfg *Config) (*sql.DB, func(), error) {
|
||||
db, err := sql.Open("postgres", string(cfg.DSN))
|
||||
if err != nil { return nil, nil, err }
|
||||
return db, func() { db.Close() }, nil
|
||||
}
|
||||
|
||||
func NewCache(cfg *Config) (*redis.Client, func(), error) {
|
||||
c := redis.NewClient(&redis.Options{Addr: cfg.CacheAddr})
|
||||
return c, func() { c.Close() }, nil
|
||||
}
|
||||
```
|
||||
|
||||
Wire generates something like:
|
||||
|
||||
```go
|
||||
func InitApp() (*App, func(), error) {
|
||||
cfg := NewConfig()
|
||||
db, dbCleanup, err := NewDB(cfg)
|
||||
if err != nil { return nil, nil, err }
|
||||
cache, cacheCleanup, err := NewCache(cfg)
|
||||
if err != nil {
|
||||
dbCleanup() // already-built cleanups run on partial failure
|
||||
return nil, nil, err
|
||||
}
|
||||
app := NewApp(db, cache)
|
||||
return app, func() {
|
||||
cacheCleanup() // reverse order
|
||||
dbCleanup()
|
||||
}, nil
|
||||
}
|
||||
```
|
||||
|
||||
**Caller pattern** — guard against nil cleanup on construction failure:
|
||||
|
||||
```go
|
||||
app, cleanup, err := InitApp()
|
||||
if err != nil { log.Fatal(err) }
|
||||
defer cleanup()
|
||||
```
|
||||
|
||||
Wire always returns a non-nil cleanup function when construction succeeds. If construction fails midway, the returned `cleanup` is nil — guard before calling.
|
||||
|
||||
## Multiple Injectors in One Package
|
||||
|
||||
A package can contain multiple injector functions. Each must live in a file with `//go:build wireinject`. All generated functions land in `wire_gen.go` in the same package.
|
||||
|
||||
```go
|
||||
//go:build wireinject
|
||||
|
||||
package main
|
||||
|
||||
// Production injector
|
||||
func InitProdApp() (*App, func(), error) {
|
||||
wire.Build(ProdSet, NewApp)
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
// Development injector with debug providers
|
||||
func InitDevApp() (*App, func(), error) {
|
||||
wire.Build(DevSet, NewApp)
|
||||
return nil, nil, nil
|
||||
}
|
||||
```
|
||||
|
||||
Select at runtime with a flag, or at build time with separate `//go:build prod` / `//go:build !prod` constraints on the injector files.
|
||||
|
||||
## wire.NewSet Nesting Strategies
|
||||
|
||||
Sets can contain other sets, building a hierarchy that mirrors your package structure.
|
||||
|
||||
```go
|
||||
// pkg/config/wire.go
|
||||
var ConfigSet = wire.NewSet(NewConfig)
|
||||
|
||||
// pkg/infra/wire.go
|
||||
var InfraSet = wire.NewSet(
|
||||
config.ConfigSet, // embed upstream set
|
||||
NewDB,
|
||||
NewCache,
|
||||
)
|
||||
|
||||
// pkg/service/wire.go
|
||||
var ServiceSet = wire.NewSet(
|
||||
NewUserService,
|
||||
wire.Bind(new(UserStore), new(*UserRepo)),
|
||||
)
|
||||
|
||||
// wire.go (injector)
|
||||
wire.Build(infra.InfraSet, service.ServiceSet, NewApp)
|
||||
```
|
||||
|
||||
**Library set stability rules** (from upstream best practices):
|
||||
|
||||
- Safe: replace one provider with another that has the same or fewer inputs, in the same release.
|
||||
- Safe: introduce a brand-new output type not previously provided.
|
||||
- Breaking: add a new required input to a provider — downstream injectors cannot satisfy it.
|
||||
- Breaking: remove a provided output type — downstream injectors that depend on it fail.
|
||||
- Breaking: add a type that the injector already provides — Wire reports a duplicate.
|
||||
|
||||
## `wire:"-"` Exclusion Tag
|
||||
|
||||
Exclude a struct field from `wire.Struct` injection by tagging it:
|
||||
|
||||
```go
|
||||
type Server struct {
|
||||
Logger *zap.Logger
|
||||
DB *sql.DB
|
||||
mu sync.Mutex `wire:"-"` // unexported — auto-excluded
|
||||
Timeout time.Duration `wire:"-"` // exported but opt-out
|
||||
}
|
||||
|
||||
wire.Struct(new(Server), "*") // injects Logger and DB; skips mu and Timeout
|
||||
```
|
||||
|
||||
Unexported fields are always skipped regardless of the tag.
|
||||
|
||||
## Common Codegen Errors
|
||||
|
||||
| Error message | Root cause | Fix |
|
||||
| --- | --- | --- |
|
||||
| `no provider found for TYPE` | A dependency is not provided by any set in `wire.Build` | Add the missing provider or set |
|
||||
| `multiple bindings for TYPE` | Two providers return the same type | Use named types or remove the duplicate |
|
||||
| `argument N has no provider for TYPE` | An interface is requested but no `wire.Bind` maps to it | Add `wire.Bind(new(Iface), new(*Impl))` to a set |
|
||||
| `cycle detected` | A → B → A circular dependency | Break the cycle by introducing an interface or factory |
|
||||
| `wire.Build used outside of injector function` | `wire.Build` called from a non-injector function | Only call `wire.Build` inside functions with the build tag |
|
||||
| duplicate symbol / redeclared in this block | Injector file is missing `//go:build wireinject` | Add the build tag as the first line |
|
||||
|
||||
## Codegen Flags
|
||||
|
||||
```bash
|
||||
# Specify output file prefix (default: wire_gen)
|
||||
wire -output_file_prefix=init gen ./cmd/server
|
||||
|
||||
# Apply build tags during generation
|
||||
wire -tags=integration gen ./...
|
||||
|
||||
# Prepend a header file (e.g., license comment) to generated output
|
||||
wire -header_file=hack/boilerplate.go.txt gen ./...
|
||||
```
|
||||
|
||||
## `panic(wire.Build(...))` Alternate Syntax
|
||||
|
||||
Wire accepts either a dummy return or a `panic` call as the injector body. The `panic` form avoids writing zero-value returns for complex types:
|
||||
|
||||
```go
|
||||
// Preferred when return types are complex or error-prone to zero-initialize
|
||||
func InitApp(ctx context.Context) (*App, func(), error) {
|
||||
panic(wire.Build(AppSet))
|
||||
}
|
||||
```
|
||||
|
||||
Wire detects both forms and replaces the body during codegen. The `panic` is never reached in the compiled binary — only the generated `wire_gen.go` version is compiled.
|
||||
|
||||
## Accepting External Values as Injector Arguments
|
||||
|
||||
When a value is constructed before wire runs (e.g., parsed flags, an `http.Client` from a test), pass it as a parameter to the injector rather than providing it from within the graph:
|
||||
|
||||
```go
|
||||
//go:build wireinject
|
||||
|
||||
func InitApp(cfg *Config) (*App, func(), error) {
|
||||
wire.Build(InfraSet, ServiceSet, NewApp)
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
// main.go
|
||||
cfg := parseFlags()
|
||||
app, cleanup, err := InitApp(cfg)
|
||||
```
|
||||
|
||||
Wire treats injector parameters as pre-built providers — they satisfy dependencies without needing a `wire.NewSet` entry.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Symbol | Purpose |
|
||||
| --- | --- |
|
||||
| `wire.NewSet(providers...)` | Group providers into a reusable set |
|
||||
| `wire.Build(sets...)` | Declare injector body (codegen replaces it) |
|
||||
| `wire.Bind(new(Iface), new(*Concrete))` | Bind interface to concrete type |
|
||||
| `wire.Struct(new(T), "Field", ...)` | Inject struct fields from the graph |
|
||||
| `wire.Struct(new(T), "*")` | Inject all non-excluded fields |
|
||||
| `wire.Value(expr)` | Bind a constant expression (no fn calls/channels) |
|
||||
| `wire.InterfaceValue(new(I), value)` | Bind a value to an interface type |
|
||||
| `wire.FieldsOf(new(T), "Field", ...)` | Promote struct fields as individual graph nodes |
|
||||
| `//go:build wireinject` | Build tag: exclude injector stub from binary |
|
||||
| `wire_gen.go` | Generated output — commit, never edit |
|
||||
| `wire ./...` | Regenerate all injectors in the module |
|
||||
| `wire check ./...` | Validate graph without regenerating |
|
||||
@@ -0,0 +1,264 @@
|
||||
# Recipes — google/wire
|
||||
|
||||
End-to-end examples. Each recipe is self-contained.
|
||||
|
||||
## HTTP Server with Postgres and Redis
|
||||
|
||||
A typical service: parsed config → DB (with cleanup) → Redis (with cleanup) → repo → service → HTTP server.
|
||||
|
||||
```
|
||||
myapp/
|
||||
├── config/
|
||||
│ ├── config.go
|
||||
│ └── wire.go
|
||||
├── infra/
|
||||
│ ├── db.go
|
||||
│ ├── cache.go
|
||||
│ └── wire.go
|
||||
├── repo/
|
||||
│ ├── user.go
|
||||
│ └── wire.go
|
||||
├── service/
|
||||
│ ├── user.go
|
||||
│ └── wire.go
|
||||
├── transport/
|
||||
│ ├── handler.go
|
||||
│ └── wire.go
|
||||
├── wire.go // injector — //go:build wireinject
|
||||
├── wire_gen.go // generated — commit this
|
||||
└── main.go
|
||||
```
|
||||
|
||||
```go
|
||||
// config/config.go
|
||||
type Config struct {
|
||||
Addr string
|
||||
DSN string
|
||||
CacheAddr string
|
||||
}
|
||||
|
||||
func NewConfig() *Config {
|
||||
return &Config{
|
||||
Addr: env("ADDR", ":8080"),
|
||||
DSN: mustEnv("DATABASE_URL"),
|
||||
CacheAddr: env("REDIS_ADDR", "localhost:6379"),
|
||||
}
|
||||
}
|
||||
|
||||
// config/wire.go
|
||||
var ConfigSet = wire.NewSet(NewConfig)
|
||||
```
|
||||
|
||||
```go
|
||||
// infra/db.go
|
||||
func NewDB(cfg *config.Config) (*sql.DB, func(), error) {
|
||||
db, err := sql.Open("postgres", cfg.DSN)
|
||||
if err != nil { return nil, nil, err }
|
||||
if err := db.Ping(); err != nil { db.Close(); return nil, nil, err }
|
||||
return db, func() { db.Close() }, nil
|
||||
}
|
||||
|
||||
// infra/cache.go
|
||||
func NewRedis(cfg *config.Config) (*redis.Client, func(), error) {
|
||||
c := redis.NewClient(&redis.Options{Addr: cfg.CacheAddr})
|
||||
if err := c.Ping(context.Background()).Err(); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return c, func() { c.Close() }, nil
|
||||
}
|
||||
|
||||
// infra/wire.go
|
||||
var InfraSet = wire.NewSet(NewDB, NewRedis)
|
||||
```
|
||||
|
||||
```go
|
||||
// repo/user.go
|
||||
type UserStore interface {
|
||||
GetUser(ctx context.Context, id int64) (*User, error)
|
||||
}
|
||||
|
||||
type PostgresUserRepo struct{ db *sql.DB }
|
||||
|
||||
func NewUserRepo(db *sql.DB) *PostgresUserRepo { return &PostgresUserRepo{db: db} }
|
||||
|
||||
// repo/wire.go
|
||||
var RepoSet = wire.NewSet(
|
||||
NewUserRepo,
|
||||
wire.Bind(new(UserStore), new(*PostgresUserRepo)),
|
||||
)
|
||||
```
|
||||
|
||||
```go
|
||||
// service/user.go
|
||||
type UserService struct {
|
||||
store repo.UserStore
|
||||
cache *redis.Client
|
||||
}
|
||||
|
||||
func NewUserService(store repo.UserStore, cache *redis.Client) *UserService {
|
||||
return &UserService{store: store, cache: cache}
|
||||
}
|
||||
|
||||
// service/wire.go
|
||||
var ServiceSet = wire.NewSet(NewUserService)
|
||||
```
|
||||
|
||||
```go
|
||||
// wire.go
|
||||
//go:build wireinject
|
||||
|
||||
package main
|
||||
|
||||
func InitApp() (*transport.Handler, func(), error) {
|
||||
wire.Build(
|
||||
config.ConfigSet,
|
||||
infra.InfraSet,
|
||||
repo.RepoSet,
|
||||
service.ServiceSet,
|
||||
transport.NewHandler,
|
||||
)
|
||||
return nil, nil, nil
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
// main.go
|
||||
func main() {
|
||||
handler, cleanup, err := InitApp()
|
||||
if err != nil { log.Fatal(err) }
|
||||
defer cleanup()
|
||||
|
||||
srv := &http.Server{Addr: ":8080", Handler: handler}
|
||||
log.Fatal(srv.ListenAndServe())
|
||||
}
|
||||
```
|
||||
|
||||
## Multiple Build Variants (Prod vs Dev)
|
||||
|
||||
Use separate injector files with `//go:build` constraints to select different provider sets at build time.
|
||||
|
||||
```go
|
||||
// wire_prod.go
|
||||
//go:build wireinject && !dev
|
||||
|
||||
package main
|
||||
|
||||
func InitApp() (*App, func(), error) {
|
||||
wire.Build(ProdSet, NewApp)
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
// wire_dev.go
|
||||
//go:build wireinject && dev
|
||||
|
||||
package main
|
||||
|
||||
func InitApp() (*App, func(), error) {
|
||||
wire.Build(DevSet, NewApp) // DevSet swaps real DB for in-memory SQLite
|
||||
return nil, nil, nil
|
||||
}
|
||||
```
|
||||
|
||||
Use `-output_file_prefix` to write separate output files — both commands would otherwise overwrite the same `wire_gen.go`:
|
||||
|
||||
```bash
|
||||
wire -tags dev -output_file_prefix=wire_gen_dev gen .
|
||||
wire -output_file_prefix=wire_gen_prod gen .
|
||||
```
|
||||
|
||||
Add build constraints to the generated files so only one compiles per build:
|
||||
|
||||
```go
|
||||
// wire_gen_prod.go — add at the top (after wire writes it)
|
||||
//go:build !dev
|
||||
|
||||
// wire_gen_dev.go — add at the top
|
||||
//go:build dev
|
||||
```
|
||||
|
||||
Commit both generated files. At build time, only the matching file is compiled.
|
||||
|
||||
## Cleanup-Heavy Graph
|
||||
|
||||
When several providers need shutdown coordination, wire's reverse-order cleanup is essential.
|
||||
|
||||
```go
|
||||
// Providers return (T, func(), error)
|
||||
func NewDBPool(cfg *Config) (*pgxpool.Pool, func(), error) {
|
||||
pool, err := pgxpool.New(context.Background(), cfg.DSN)
|
||||
if err != nil { return nil, nil, err }
|
||||
return pool, func() { pool.Close() }, nil
|
||||
}
|
||||
|
||||
func NewOTelExporter(cfg *Config) (*otlptrace.Exporter, func(), error) {
|
||||
exp, err := otlptracegrpc.New(context.Background(), ...)
|
||||
if err != nil { return nil, nil, err }
|
||||
return exp, func() { exp.Shutdown(context.Background()) }, nil
|
||||
}
|
||||
|
||||
func NewTracerProvider(exp *otlptrace.Exporter) (*trace.TracerProvider, func(), error) {
|
||||
tp := trace.NewTracerProvider(trace.WithBatcher(exp))
|
||||
return tp, func() { tp.Shutdown(context.Background()) }, nil
|
||||
}
|
||||
```
|
||||
|
||||
Wire generates shutdown in reverse: `TracerProvider` → `OTelExporter` → `DBPool`. Each cleanup runs before its dependencies shut down — guaranteeing in-flight spans are flushed before the exporter closes.
|
||||
|
||||
## Embedding Wire in a CLI
|
||||
|
||||
Wire produces a struct, not an app framework. You control the lifecycle:
|
||||
|
||||
```go
|
||||
// wire.go
|
||||
//go:build wireinject
|
||||
|
||||
package cmd
|
||||
|
||||
func InitServer(cfg *Config) (*http.Server, func(), error) {
|
||||
wire.Build(InfraSet, ServiceSet, NewHTTPServer)
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
// cmd/serve.go
|
||||
func runServe(cfg *Config) error {
|
||||
srv, cleanup, err := InitServer(cfg)
|
||||
if err != nil { return err }
|
||||
defer cleanup()
|
||||
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
go func() {
|
||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}()
|
||||
<-quit
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
return srv.Shutdown(ctx)
|
||||
}
|
||||
```
|
||||
|
||||
Unlike `fx.Run()`, wire does not manage the lifecycle loop. Implement signal handling and graceful shutdown explicitly. This is a feature for CLI tools that spin up short-lived services and need precise control over the shutdown sequence.
|
||||
|
||||
## Passing External Values to Wire
|
||||
|
||||
Values built before wire runs (parsed config, test doubles) become injector parameters:
|
||||
|
||||
```go
|
||||
//go:build wireinject
|
||||
|
||||
// cfg is resolved outside the graph — treated as a provided *Config
|
||||
func InitApp(cfg *Config) (*App, func(), error) {
|
||||
wire.Build(InfraSet, ServiceSet, NewApp)
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
// main.go
|
||||
cfg, err := config.Load()
|
||||
if err != nil { log.Fatal(err) }
|
||||
app, cleanup, err := InitApp(cfg)
|
||||
```
|
||||
|
||||
The parameter `cfg *Config` satisfies any downstream provider that requests `*Config` — no `wire.Value` or extra set entry needed.
|
||||
@@ -0,0 +1,173 @@
|
||||
# Testing — google/wire
|
||||
|
||||
Wire generates plain Go constructor calls, so tests work directly on the constructor layer — no container API to learn.
|
||||
|
||||
## Unit Tests: Plain Constructor Injection
|
||||
|
||||
The generated code has no wire dependency. Test constructors directly:
|
||||
|
||||
```go
|
||||
func TestUserService_GetUser(t *testing.T) {
|
||||
mockStore := &MockUserStore{users: map[int64]*User{1: &User{ID: 1, Name: "Alice"}}}
|
||||
cache := newTestRedis(t)
|
||||
svc := service.NewUserService(mockStore, cache)
|
||||
|
||||
u, err := svc.GetUser(context.Background(), 1)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Alice", u.Name)
|
||||
}
|
||||
```
|
||||
|
||||
Pass mocks directly as constructor arguments. No wire, no container, no file to generate. This is the idiomatic approach for unit tests.
|
||||
|
||||
## Test Injectors: Swapping Providers
|
||||
|
||||
For integration or component tests where you want the full wired graph but with selected dependencies replaced, create a test-only injector in a `_test.go` file.
|
||||
|
||||
```go
|
||||
// app_test.go
|
||||
//go:build wireinject
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"github.com/google/wire"
|
||||
)
|
||||
|
||||
// TestSet replaces real infra with in-memory fakes
|
||||
var TestSet = wire.NewSet(
|
||||
NewTestConfig,
|
||||
NewInMemoryUserStore,
|
||||
wire.Bind(new(repo.UserStore), new(*InMemoryUserStore)),
|
||||
NewTestRedis,
|
||||
)
|
||||
|
||||
func InitTestApp(t *testing.T) (*App, func(), error) {
|
||||
wire.Build(TestSet, service.ServiceSet, NewApp)
|
||||
return nil, nil, nil
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
// app_integration_test.go
|
||||
//go:build !wireinject // compiles when the wireinject tag is NOT set
|
||||
|
||||
package main
|
||||
|
||||
func TestApp_GetUser(t *testing.T) {
|
||||
app, cleanup, err := InitTestApp(t)
|
||||
require.NoError(t, err)
|
||||
defer cleanup()
|
||||
|
||||
// test against the fully-wired app with fake dependencies
|
||||
u, err := app.GetUser(context.Background(), 1)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, u)
|
||||
}
|
||||
```
|
||||
|
||||
Run `wire ./...` to generate `wire_gen.go` — the test injector is included because the `_test.go` file is compiled as part of the package during `go test`.
|
||||
|
||||
**Key pattern from upstream best practices:** Prefer creating a test-only provider set over passing mocks as injector arguments (though both work). The set approach keeps the test injector composable.
|
||||
|
||||
## Passing Mocks as Injector Arguments
|
||||
|
||||
An alternative to a test set: pass the mock directly as an injector parameter. Wire treats it as a pre-built provider.
|
||||
|
||||
```go
|
||||
//go:build wireinject
|
||||
|
||||
func InitTestApp(store repo.UserStore) (*App, func(), error) {
|
||||
wire.Build(config.ConfigSet, service.ServiceSet, NewApp)
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
// Test
|
||||
func TestApp(t *testing.T) {
|
||||
mock := &MockUserStore{}
|
||||
app, cleanup, err := InitTestApp(mock)
|
||||
require.NoError(t, err)
|
||||
defer cleanup()
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Use this form when you only need to replace one or two dependencies and a full `TestSet` is overkill.
|
||||
|
||||
## CI: Detecting Stale `wire_gen.go`
|
||||
|
||||
If `wire_gen.go` is not regenerated after a provider change, CI builds pass but the graph is wrong. Enforce freshness in CI:
|
||||
|
||||
```bash
|
||||
# Option 1: re-run wire and check for diffs
|
||||
wire ./...
|
||||
git diff --exit-code -- '**/wire_gen.go'
|
||||
```
|
||||
|
||||
```yaml
|
||||
# .github/workflows/ci.yml
|
||||
- name: Check wire_gen.go is up-to-date
|
||||
run: |
|
||||
go install github.com/google/wire/cmd/wire@v0.7.0
|
||||
wire ./...
|
||||
git diff --exit-code -- '**/wire_gen.go'
|
||||
```
|
||||
|
||||
```bash
|
||||
# Option 2: use wire check (verifies graph without regenerating)
|
||||
wire check ./...
|
||||
```
|
||||
|
||||
`wire check` exits non-zero if the graph is inconsistent but does **not** update `wire_gen.go`. Use it for a fast graph-validity check without modifying files.
|
||||
|
||||
## Testing Interface Bindings
|
||||
|
||||
`wire.Bind` can be used in test sets to bind a fake to the same interface:
|
||||
|
||||
```go
|
||||
// Fake implements the same interface as the real provider
|
||||
type FakeMailer struct{ sent []string }
|
||||
func (f *FakeMailer) Send(to, body string) error { f.sent = append(f.sent, to); return nil }
|
||||
|
||||
var TestMailerSet = wire.NewSet(
|
||||
NewFakeMailer,
|
||||
wire.Bind(new(notification.Mailer), new(*FakeMailer)),
|
||||
)
|
||||
|
||||
var TestSet = wire.NewSet(
|
||||
TestMailerSet,
|
||||
realServiceSet, // everything else is real
|
||||
)
|
||||
```
|
||||
|
||||
This keeps the test injector narrow — only the Mailer is faked; the rest of the graph is real.
|
||||
|
||||
## Table-Driven Tests Without Wire
|
||||
|
||||
Wire is an initialization tool. Once the object graph is built, table-driven tests on individual services need no wire involvement:
|
||||
|
||||
```go
|
||||
func TestUserService(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
id int64
|
||||
users map[int64]*User
|
||||
want string
|
||||
err bool
|
||||
}{
|
||||
{"found", 1, map[int64]*User{1: &User{Name: "Alice"}}, "Alice", false},
|
||||
{"not found", 99, nil, "", true},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
svc := service.NewUserService(&MockUserStore{users: tc.users}, nil)
|
||||
u, err := svc.GetUser(context.Background(), tc.id)
|
||||
if tc.err { require.Error(t, err); return }
|
||||
assert.Equal(t, tc.want, u.Name)
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Wire has no role here — the injector was only needed to build the object graph in `main` (or in an integration test). Unit tests construct dependencies directly.
|
||||
Reference in New Issue
Block a user