A list of patterns for stubbing out dependencies in unit tests.
Problem: We have a dependency that we don't want to actually run in our unit tests.
Define an interface that abstracts this dependency, and implement a stub struct that implements this interface, then swap this into your struct at test time.
type MathSolver interface {
Resolve(ctx context.Context, expression string) (float64, error)
}
type Processor struct {
Solver MathSolver
}
func (p Processor) ProcessExpression(ctx context.Context, r io.Reader) (float64, error) {
// calls p.Solver.Resolve()
}Create a stub struct that implements the MathSolver interface.
type MathSolverStub struct {}
func (ms MathSolverStub) Resolve(ctx context.Context, expression string) (float64, error) {
// stub implementation
}In our test we use the stub instead of the real thing.
func TestProcessorProcessExpression(t *testing.T) {
p := Processor{MathSolverStub{}}
// call p.ProcessExpression()
}Problem: You have a wide interface, and its annoying to implement every method for the stub.
type Entities interface {
GetUser(id string) (User, error)
GetPets(userID string) ([]Pet, error)
GetChildren(userID string) ([]Person, error)
GetFriends(userID string) ([]Person, error)
SaveUser(user User) error
}type Logic struct {
Entities Entities
}
func (l Logic) GetPetNames(userId string) ([]string, error) {
// calls l.Entities.GetPets
}Rather than creating a stub that implements every single method on Entities just to test GetPets, you can write a stub struct that only implements the method you need:
type GetPetNamesStub struct {
Entities
}
func (ps GetPetNamesStub) GetPets(userId string) ([]Pet, error) {}By embedding the Entities in GetPetNamesStub, it automatically satisfies the interface.
In our test make sure to only call methods that are implemented, otherwise you get a runtime error. This is one of the weaknesses of this approach.
func TestLogicGetPetNames(t *testing.T) {
l := Logic{GetPetNamesStub{}}
// only call l.GetPetNames
}Problem: You want more granular control over which parts of the interface to stub out versus which parts to keep.
Lets keep using the Entities interface defined above.
For each method defined on Entities, we define a function field with a matching signature on our stub struct.
We then make EntitiesStub implement the Entities interface by defining the methods. In each method, we involve the associated function field.
type EntitiesStub struct {
getUser func(id string) (User, error)
getPets func(userID string) ([]Pet, error)
getChildren func(userID string) ([]Person, error)
getFriends func(userID string) ([]Person, error)
saveUser func(user User) error
}
func (es EntitiesStub) GetUser(id string) (User, error) {
return es.getUser(id)
}
func (es EntitiesStub) GetPets(userID string) ([]Pet, error) {
return es.getPets(userID)
}In our test:
func TestLogicGetPetNames(t *testing.T) {
tests := []struct {
name string
getPets func(userID string) ([]Pet, error)
userID string
petNames []string
errMsg string
}{
{
name: "case1",
getPets: func(userID string) ([]Pet, error) {
return []Pet{{Name: "Bubbles"}}, nil
},
userID: "1",
petNames: []string{"Bubbles"},
errMsg: ""
},
{
name: "case2",
getPets: func(userID string) ([]Pet, error) {
return nil, errors.New("invalid id: 3")
},
userID: "3",
petNames: nil,
errMsg: "invalid id: 3"
},
}
l := Logic{}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// update the function field with the test case's version
l.Entities = EntitiesStub{getPets: test.getPets}
// l.GetPetNames()
})
}
}Stubs and Mocks are different concepts, but often used interchangeably. A stub returns a fake value for a given input. A mock validates that a set of calls happen in the expected order with the expected inputs.