174 lines
5.0 KiB
Markdown
174 lines
5.0 KiB
Markdown
# 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.
|