diff --git a/README.md b/README.md index f576941..d60c856 100644 --- a/README.md +++ b/README.md @@ -453,3 +453,24 @@ You may not: `*Greeter` will be broken. - Add a provider for `io.Writer` to `GreeterSet`. Injectors might already have a provider for `io.Writer` which might conflict with this one. + +### Mocking + +There are two approaches for creating an injected app with mocked dependencies. +Examples of both approaches are shown +[here](https://github.com/google/go-cloud/tree/master/wire/internal/wire/testdata/ExampleWithMocks/foo). + +#### Approach A: Pass mocks to the injector + +Create a test-only injector that takes all of the mocks as arguments; the +argument types must be the interface types the mocks are mocking. `wire.Build` +can't include providers for the mocked dependencies without creating conflicts, +so if you're using provider set(s) you will need to define one that doesn't +include the mocked types. + +#### Approach B: Return the mocks from the injector + +Create a new struct that includes the app plus all of the dependencies you want +to mock. Create a test-only injector that returns this struct, give it providers +for the concrete mock types, and use `wire.Bind` to tell Wire that the +concrete mock types should be used to fulfill the appropriate interface. diff --git a/internal/wire/testdata/ExampleWithMocks/foo/foo.go b/internal/wire/testdata/ExampleWithMocks/foo/foo.go new file mode 100644 index 0000000..e51ad74 --- /dev/null +++ b/internal/wire/testdata/ExampleWithMocks/foo/foo.go @@ -0,0 +1,120 @@ +// Copyright 2018 The Go Cloud Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// This test demonstrates how to use mocks with wire. + +package main + +import ( + "fmt" + "time" + + "github.com/google/go-cloud/wire" +) + +func main() { + // Create a "real" greeter. + // Greet() will include the real current time, so elide it for repeatable + // tests. + fmt.Printf("Real time greeting: %s [current time elided]\n", initApp().Greet()[0:15]) + + // There are two approaches for creating an app with mocks. + + // Approach A: create the mocks manually, and pass them to an injector. + // This approach is useful if you need to prime the mocks beforehand. + fmt.Println("Approach A") + mt := newMockTimer() + mockedApp := initMockedAppFromArgs(mt) + fmt.Println(mockedApp.Greet()) // prints greeting with time = zero time + mt.T = mt.T.AddDate(1999, 0, 0) + fmt.Println(mockedApp.Greet()) // prints greeting with time = year 2000 + + // Approach B: allow the injector to create the mocks, and return a struct + // that includes the resulting app plus the mocks. + fmt.Println("Approach B") + appWithMocks := initMockedApp() + fmt.Println(appWithMocks.app.Greet()) // prints greeting with time = zero time + appWithMocks.mt.T = appWithMocks.mt.T.AddDate(999, 0, 0) + fmt.Println(appWithMocks.app.Greet()) // prints greeting with time = year 1000 +} + +// appSet is a provider set for creating a real app. +var appSet = wire.NewSet( + app{}, + greeter{}, + wire.InterfaceValue(new(timer), realTime{}), +) + +// appSetWithoutMocks is a provider set for creating an app with mocked +// dependencies. The mocked dependencies are omitted and must be provided as +// arguments to the injector. +// It is used for Approach A. +var appSetWithoutMocks = wire.NewSet( + app{}, + greeter{}, +) + +// mockAppSet is a provider set for creating a mocked app, including the mocked +// dependencies. +// It is used for Approach B. +var mockAppSet = wire.NewSet( + app{}, + greeter{}, + appWithMocks{}, + // For each mocked dependency, add a provider and use wire.Bind to bind + // the concrete type to the relevant interface. + newMockTimer, + wire.Bind(new(timer), new(mockTimer)), +) + +type timer interface { + Now() time.Time +} + +// realTime implements timer with the real time. +type realTime struct{} + +func (realTime) Now() time.Time { return time.Now() } + +// mockTimer implements timer using a mocked time. +type mockTimer struct { + T time.Time +} + +func newMockTimer() *mockTimer { return &mockTimer{} } +func (m *mockTimer) Now() time.Time { return m.T } + +// greeter issues greetings with the time provided by T. +type greeter struct { + T timer +} + +func (g greeter) Greet() string { + return fmt.Sprintf("Good day! It is %v", g.T.Now()) +} + +type app struct { + g greeter +} + +func (a app) Greet() string { + return a.g.Greet() +} + +// appWithMocks is used for Approach B, to return the app plus its mocked +// dependencies. +type appWithMocks struct { + app app + mt *mockTimer +} diff --git a/internal/wire/testdata/ExampleWithMocks/foo/wire.go b/internal/wire/testdata/ExampleWithMocks/foo/wire.go new file mode 100644 index 0000000..5d9682e --- /dev/null +++ b/internal/wire/testdata/ExampleWithMocks/foo/wire.go @@ -0,0 +1,42 @@ +// Copyright 2018 The Go Cloud Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//+build wireinject + +package main + +import ( + "github.com/google/go-cloud/wire" +) + +// initApp returns a real app. +func initApp() *app { + wire.Build(appSet) + return nil +} + +// initMockedAppFromArgs returns an app with mocked dependencies provided via +// arguments (Approach A). Note that the argument's type is the interface +// type (timer), but the concrete mock type should be passed. +func initMockedAppFromArgs(mt timer) *app { + wire.Build(appSetWithoutMocks) + return nil +} + +// initMockedApp returns an app with its mocked dependencies, created +// via providers (Approach B). +func initMockedApp() *appWithMocks { + wire.Build(mockAppSet) + return nil +} diff --git a/internal/wire/testdata/ExampleWithMocks/pkg b/internal/wire/testdata/ExampleWithMocks/pkg new file mode 100644 index 0000000..f7a5c8c --- /dev/null +++ b/internal/wire/testdata/ExampleWithMocks/pkg @@ -0,0 +1 @@ +example.com/foo diff --git a/internal/wire/testdata/ExampleWithMocks/want/program_out.txt b/internal/wire/testdata/ExampleWithMocks/want/program_out.txt new file mode 100644 index 0000000..8f74a8e --- /dev/null +++ b/internal/wire/testdata/ExampleWithMocks/want/program_out.txt @@ -0,0 +1,7 @@ +Real time greeting: Good day! It is [current time elided] +Approach A +Good day! It is 0001-01-01 00:00:00 +0000 UTC +Good day! It is 2000-01-01 00:00:00 +0000 UTC +Approach B +Good day! It is 0001-01-01 00:00:00 +0000 UTC +Good day! It is 1000-01-01 00:00:00 +0000 UTC diff --git a/internal/wire/testdata/ExampleWithMocks/want/wire_gen.go b/internal/wire/testdata/ExampleWithMocks/want/wire_gen.go new file mode 100644 index 0000000..86521f3 --- /dev/null +++ b/internal/wire/testdata/ExampleWithMocks/want/wire_gen.go @@ -0,0 +1,48 @@ +// Code generated by Wire. DO NOT EDIT. + +//go:generate wire +//+build !wireinject + +package main + +// Injectors from wire.go: + +func initApp() *app { + mainTimer := _wireRealTimeValue + mainGreeter := greeter{ + T: mainTimer, + } + mainApp := &app{ + g: mainGreeter, + } + return mainApp +} + +var ( + _wireRealTimeValue = realTime{} +) + +func initMockedAppFromArgs(mt timer) *app { + mainGreeter := greeter{ + T: mt, + } + mainApp := &app{ + g: mainGreeter, + } + return mainApp +} + +func initMockedApp() *appWithMocks { + mainMockTimer := newMockTimer() + mainGreeter := greeter{ + T: mainMockTimer, + } + mainApp := app{ + g: mainGreeter, + } + mainAppWithMocks := &appWithMocks{ + app: mainApp, + mt: mainMockTimer, + } + return mainAppWithMocks +}