145 lines
4.1 KiB
Markdown
145 lines
4.1 KiB
Markdown
# Testing with uber-go/fx
|
|
|
|
`go.uber.org/fx/fxtest` integrates fx applications with `*testing.T`: errors fail the test instead of crashing the process, and lifecycle teardown is registered automatically.
|
|
|
|
## Pulling a value out of the graph with `fx.Populate`
|
|
|
|
```go
|
|
func TestUserService_Create(t *testing.T) {
|
|
var svc *UserService
|
|
|
|
app := fxtest.New(t,
|
|
fx.Provide(
|
|
func() Database { return &fakeDatabase{} },
|
|
NewUserService,
|
|
),
|
|
fx.Populate(&svc),
|
|
)
|
|
defer app.RequireStop()
|
|
app.RequireStart()
|
|
|
|
require.NoError(t, svc.Create(context.Background(), "alice@example.com"))
|
|
}
|
|
```
|
|
|
|
`fx.Populate(&svc)` fills `svc` with the value the graph would resolve. It replaces ad-hoc `fx.Invoke(func(s *UserService) { svc = s })` patterns.
|
|
|
|
## `fx.Replace` to swap a real dependency for a fake
|
|
|
|
```go
|
|
func TestServer_HandlesDBError(t *testing.T) {
|
|
var srv *http.Server
|
|
fakeDB := &erroringDatabase{}
|
|
|
|
app := fxtest.New(t,
|
|
ProductionModule, // the real wiring
|
|
fx.Replace(fx.Annotate(fakeDB, fx.As(new(Database)))),
|
|
fx.Populate(&srv),
|
|
)
|
|
defer app.RequireStop()
|
|
app.RequireStart()
|
|
|
|
// Drive the server with a fake DB
|
|
rec := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/users", nil)
|
|
srv.Handler.ServeHTTP(rec, req)
|
|
require.Equal(t, http.StatusInternalServerError, rec.Code)
|
|
}
|
|
```
|
|
|
|
`fx.Replace` works even when the original provider is buried inside a module — it overrides the resolved type without rewriting the module.
|
|
|
|
## Standalone lifecycle for a unit test
|
|
|
|
`fxtest.NewLifecycle(t)` gives you an `fx.Lifecycle` outside the `fx.New` machinery, useful for testing a single constructor that registers hooks:
|
|
|
|
```go
|
|
func TestWorker_StartStop(t *testing.T) {
|
|
lc := fxtest.NewLifecycle(t)
|
|
|
|
worker := NewWorker(lc, zaptest.NewLogger(t))
|
|
require.NotNil(t, worker)
|
|
|
|
lc.RequireStart() // runs OnStart hooks
|
|
require.True(t, worker.IsRunning())
|
|
|
|
lc.RequireStop() // runs OnStop hooks
|
|
require.False(t, worker.IsRunning())
|
|
}
|
|
```
|
|
|
|
This is the lightest test for a constructor — no full graph, no `fx.New`.
|
|
|
|
## Asserting wire-time errors
|
|
|
|
```go
|
|
func TestWiring_MissingDependency(t *testing.T) {
|
|
app := fx.New(
|
|
fx.Provide(NewServer), // depends on *sql.DB which is not provided
|
|
fx.NopLogger,
|
|
)
|
|
require.Error(t, app.Err())
|
|
require.Contains(t, app.Err().Error(), "missing type: *sql.DB")
|
|
}
|
|
```
|
|
|
|
Use `fx.New` (not `fxtest.New`) when you _expect_ the wiring to fail — `fxtest.New` would call `t.Fatal`.
|
|
|
|
## Validating the production graph in CI
|
|
|
|
```go
|
|
func TestProductionGraph(t *testing.T) {
|
|
app := fx.New(
|
|
ProductionOptions(), // every fx.Provide / fx.Module the binary uses
|
|
fx.NopLogger,
|
|
)
|
|
require.NoError(t, app.Err())
|
|
}
|
|
```
|
|
|
|
`fx.New` validates the type graph without starting. The test fails before deploy on any missing-provider, cycle, or annotation mismatch.
|
|
|
|
## Test logger that captures fx events
|
|
|
|
When you want to assert on lifecycle behavior, route fx events into an in-memory observer:
|
|
|
|
```go
|
|
// go.uber.org/zap/zaptest/observer
|
|
core, recorded := observer.New(zap.InfoLevel)
|
|
log := zap.New(core)
|
|
|
|
app := fxtest.New(t,
|
|
fx.WithLogger(func() fxevent.Logger {
|
|
return &fxevent.ZapLogger{Logger: log}
|
|
}),
|
|
fx.Provide(NewWorker),
|
|
fx.Invoke(func(*Worker) {}),
|
|
)
|
|
defer app.RequireStop()
|
|
app.RequireStart()
|
|
|
|
require.NotEmpty(t, recorded.FilterMessage("OnStart hook executed").All())
|
|
```
|
|
|
|
## Testing a lifecycle hook in isolation
|
|
|
|
If a constructor returns a value _and_ registers a hook, you often want to test both halves:
|
|
|
|
```go
|
|
func TestNewServer_OnStartFailsBindError(t *testing.T) {
|
|
// Bind a port so :0 is unavailable... no, simpler: pre-bind and pass that addr
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
require.NoError(t, err)
|
|
defer listener.Close()
|
|
addr := listener.Addr().String()
|
|
|
|
cfg := &Config{Addr: addr}
|
|
lc := fxtest.NewLifecycle(t)
|
|
|
|
NewHTTPServer(lc, zaptest.NewLogger(t), cfg)
|
|
|
|
// Use Start directly (not RequireStart) so we can assert the error.
|
|
require.Error(t, lc.Start(context.Background()))
|
|
}
|
|
```
|