Skip to content

Commit 6b481cf

Browse files
committed
put a tl;dr for fakes and contracts
1 parent 25be08b commit 6b481cf

File tree

1 file changed

+124
-118
lines changed

1 file changed

+124
-118
lines changed

working-without-mocks.md

Lines changed: 124 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
# Working without mocks, stubs and spies
22

3-
This chapter delves into the world of test doubles and explores how they influence the testing and development process. We'll uncover the limitations of traditional mocks, stubs, and spies and introduce a more efficient and adaptable approach using fakes and contracts. These methods simplify testing, enhance local development experiences, and streamline the management of evolving dependencies.
3+
This chapter delves into the world of test doubles and explores how they influence the testing and development process. We'll uncover the limitations of traditional mocks, stubs, and spies and introduce a more efficient and adaptable approach using fakes and contracts.
44

5-
This is a longer chapter than normal, so as a palette cleanser, you might want to explore an [example repo first](https://github.com/quii/go-fakes-and-contracts). In particular, check out the [planner test](https://github.com/quii/go-fakes-and-contracts/blob/main/domain/planner/planner_test.go).
5+
## tl;dr
6+
7+
- Mocks, spies and stubs encourage you to encode assumptions of the behaviour of your dependencies ad-hocly in each test.
8+
- These assumptions are usually not validated beyond manual checking, so they threaten your test suite's usefulness.
9+
- Fakes and contracts give us a more sustainable method for creating test doubles with validated assumptions and better reuse than the alternatives.
10+
11+
This is a longer chapter than normal, so as a palette cleanser, you should explore an [example repo first](https://github.com/quii/go-fakes-and-contracts). In particular, check out the [planner test](https://github.com/quii/go-fakes-and-contracts/blob/main/domain/planner/planner_test.go).
612

713
---
814

@@ -22,16 +28,16 @@ It's easy to roll your eyes when people like me are pedantic about the nomenclat
2228
- Avoid latency and other performance issues
2329
- Unable to exercise non-happy path cases
2430
- Decoupling your build from another team's.
25-
- You wouldn't want to prevent deployments if an engineer in another team accidentally shipped a bug
31+
- You wouldn't want to prevent deployments if an engineer in another team accidentally shipped a bug
2632

2733
In Go, you'll typically model a dependency with an interface, then implement your version to control the behaviour in a test. **Here are the kinds of test doubles covered in this post**.
2834

2935
Given this interface of a hypothetical recipe API:
3036

3137
```go
3238
type RecipeBook interface {
33-
GetRecipes() ([]Recipe, error)
34-
AddRecipes(...Recipe) error
39+
GetRecipes() ([]Recipe, error)
40+
AddRecipes(...Recipe) error
3541
}
3642
```
3743

@@ -41,12 +47,12 @@ We can construct test doubles in various ways, depending on how we're trying to
4147

4248
```go
4349
type StubRecipeStore struct {
44-
recipes []Recipe
45-
err error
50+
recipes []Recipe
51+
err error
4652
}
4753

4854
func (s *StubRecipeStore) GetRecipes() ([]Recipe, error) {
49-
return s.recipes, s.err
55+
return s.recipes, s.err
5056
}
5157

5258
// AddRecipes omitted for brevity
@@ -59,13 +65,13 @@ stubStore := &StubRecipeStore{recipes: someRecipes}
5965

6066
```go
6167
type SpyRecipeStore struct {
62-
AddCalls [][]Recipe
63-
err error
68+
AddCalls [][]Recipe
69+
err error
6470
}
6571

6672
func (s *SpyRecipeStore) AddRecipes(r ...Recipe) error {
67-
s.AddCalls = append(s.AddCalls, r)
68-
return s.err
73+
s.AddCalls = append(s.AddCalls, r)
74+
return s.err
6975
}
7076

7177
// GetRecipes omitted for brevity
@@ -92,16 +98,16 @@ mockStore.WhenCalledWith(someRecipes).return(someError)
9298

9399
```go
94100
type FakeRecipeStore struct {
95-
recipes []Recipe
101+
recipes []Recipe
96102
}
97103

98104
func (f *FakeRecipeStore) GetRecipes() ([]Recipe, error) {
99-
return f.recipes, nil
105+
return f.recipes, nil
100106
}
101107

102108
func (f *FakeRecipeStore) AddRecipes(r ...Recipe) error {
103-
f.recipes = append(f.recipes, r...)
104-
return nil
109+
f.recipes = append(f.recipes, r...)
110+
return nil
105111
}
106112
```
107113

@@ -254,54 +260,54 @@ Here is an example of a contract for one of the APIs the system depends on
254260

255261
```go
256262
type API1Customer struct {
257-
Name string
258-
ID string
263+
Name string
264+
ID string
259265
}
260266

261267
type API1 interface {
262-
CreateCustomer(ctx context.Context, name string) (API1Customer, error)
263-
GetCustomer(ctx context.Context, id string) (API1Customer, error)
264-
UpdateCustomer(ctx context.Context, id string, name string) error
268+
CreateCustomer(ctx context.Context, name string) (API1Customer, error)
269+
GetCustomer(ctx context.Context, id string) (API1Customer, error)
270+
UpdateCustomer(ctx context.Context, id string, name string) error
265271
}
266272

267273
type API1Contract struct {
268-
NewAPI1 func() API1
274+
NewAPI1 func() API1
269275
}
270276

271277
func (c API1Contract) Test(t *testing.T) {
272-
t.Run("can create, get and update a customer", func(t *testing.T) {
273-
var (
274-
ctx = context.Background()
275-
sut = c.NewAPI1()
276-
name = "Bob"
277-
)
278-
279-
customer, err := sut.CreateCustomer(ctx, name)
280-
expect.NoErr(t, err)
281-
282-
got, err := sut.GetCustomer(ctx, customer.ID)
283-
expect.NoErr(t, err)
284-
expect.Equal(t, customer, got)
285-
286-
newName := "Robert"
287-
expect.NoErr(t, sut.UpdateCustomer(ctx, customer.ID, newName))
288-
289-
got, err = sut.GetCustomer(ctx, customer.ID)
290-
expect.NoErr(t, err)
291-
expect.Equal(t, newName, got.Name)
292-
})
293-
294-
// example of strange behaviours we didn't expect
295-
t.Run("the system will not allow you to add 'Dave' as a customer", func(t *testing.T) {
296-
var (
297-
ctx = context.Background()
298-
sut = c.NewAPI1()
299-
name = "Dave"
300-
)
301-
302-
_, err := sut.CreateCustomer(ctx, name)
303-
expect.Err(t, ErrDaveIsForbidden)
304-
})
278+
t.Run("can create, get and update a customer", func(t *testing.T) {
279+
var (
280+
ctx = context.Background()
281+
sut = c.NewAPI1()
282+
name = "Bob"
283+
)
284+
285+
customer, err := sut.CreateCustomer(ctx, name)
286+
expect.NoErr(t, err)
287+
288+
got, err := sut.GetCustomer(ctx, customer.ID)
289+
expect.NoErr(t, err)
290+
expect.Equal(t, customer, got)
291+
292+
newName := "Robert"
293+
expect.NoErr(t, sut.UpdateCustomer(ctx, customer.ID, newName))
294+
295+
got, err = sut.GetCustomer(ctx, customer.ID)
296+
expect.NoErr(t, err)
297+
expect.Equal(t, newName, got.Name)
298+
})
299+
300+
// example of strange behaviours we didn't expect
301+
t.Run("the system will not allow you to add 'Dave' as a customer", func(t *testing.T) {
302+
var (
303+
ctx = context.Background()
304+
sut = c.NewAPI1()
305+
name = "Dave"
306+
)
307+
308+
_, err := sut.CreateCustomer(ctx, name)
309+
expect.Err(t, ErrDaveIsForbidden)
310+
})
305311
}
306312
```
307313

@@ -316,47 +322,47 @@ To create our in-memory fake, we can use the contract in a test.
316322

317323
```go
318324
func TestInMemoryAPI1(t *testing.T) {
319-
API1Contract{NewAPI1: func() API1 {
320-
return inmemory.NewAPI1()
321-
}}.Test(t)
325+
API1Contract{NewAPI1: func() API1 {
326+
return inmemory.NewAPI1()
327+
}}.Test(t)
322328
}
323329
```
324330

325331
And here is the fake's code
326332

327333
```go
328334
func NewAPI1() *API1 {
329-
return &API1{customers: make(map[string]planner.API1Customer)}
335+
return &API1{customers: make(map[string]planner.API1Customer)}
330336
}
331337

332338
type API1 struct {
333-
i int
334-
customers map[string]planner.API1Customer
339+
i int
340+
customers map[string]planner.API1Customer
335341
}
336342

337343
func (a *API1) CreateCustomer(ctx context.Context, name string) (planner.API1Customer, error) {
338-
if name == "Dave" {
339-
return planner.API1Customer{}, ErrDaveIsForbidden
340-
}
341-
342-
newCustomer := planner.API1Customer{
343-
Name: name,
344-
ID: strconv.Itoa(a.i),
345-
}
346-
a.customers[newCustomer.ID] = newCustomer
347-
a.i++
348-
return newCustomer, nil
344+
if name == "Dave" {
345+
return planner.API1Customer{}, ErrDaveIsForbidden
346+
}
347+
348+
newCustomer := planner.API1Customer{
349+
Name: name,
350+
ID: strconv.Itoa(a.i),
351+
}
352+
a.customers[newCustomer.ID] = newCustomer
353+
a.i++
354+
return newCustomer, nil
349355
}
350356

351357
func (a *API1) GetCustomer(ctx context.Context, id string) (planner.API1Customer, error) {
352-
return a.customers[id], nil
358+
return a.customers[id], nil
353359
}
354360

355361
func (a *API1) UpdateCustomer(ctx context.Context, id string, name string) error {
356-
customer := a.customers[id]
357-
customer.Name = name
358-
a.customers[id] = customer
359-
return nil
362+
customer := a.customers[id]
363+
customer.Name = name
364+
a.customers[id] = customer
365+
return nil
360366
}
361367
```
362368

@@ -401,38 +407,38 @@ Returning to the `API1` example, we can create a type that implements the needed
401407

402408
```go
403409
type API1Decorator struct {
404-
delegate API1
405-
CreateCustomerFunc func(ctx context.Context, name string) (API1Customer, error)
406-
GetCustomerFunc func(ctx context.Context, id string) (API1Customer, error)
407-
UpdateCustomerFunc func(ctx context.Context, id string, name string) error
410+
delegate API1
411+
CreateCustomerFunc func(ctx context.Context, name string) (API1Customer, error)
412+
GetCustomerFunc func(ctx context.Context, id string) (API1Customer, error)
413+
UpdateCustomerFunc func(ctx context.Context, id string, name string) error
408414
}
409415

410416
// assert API1Decorator implements API1
411417
var _ API1 = &API1Decorator{}
412418

413419
func NewAPI1Decorator(delegate API1) *API1Decorator {
414-
return &API1Decorator{delegate: delegate}
420+
return &API1Decorator{delegate: delegate}
415421
}
416422

417423
func (a *API1Decorator) CreateCustomer(ctx context.Context, name string) (API1Customer, error) {
418-
if a.CreateCustomerFunc != nil {
419-
return a.CreateCustomerFunc(ctx, name)
420-
}
421-
return a.delegate.CreateCustomer(ctx, name)
424+
if a.CreateCustomerFunc != nil {
425+
return a.CreateCustomerFunc(ctx, name)
426+
}
427+
return a.delegate.CreateCustomer(ctx, name)
422428
}
423429

424430
func (a *API1Decorator) GetCustomer(ctx context.Context, id string) (API1Customer, error) {
425-
if a.GetCustomerFunc != nil {
426-
return a.GetCustomerFunc(ctx, id)
427-
}
428-
return a.delegate.GetCustomer(ctx, id)
431+
if a.GetCustomerFunc != nil {
432+
return a.GetCustomerFunc(ctx, id)
433+
}
434+
return a.delegate.GetCustomer(ctx, id)
429435
}
430436

431437
func (a *API1Decorator) UpdateCustomer(ctx context.Context, id string, name string) error {
432-
if a.UpdateCustomerFunc != nil {
433-
return a.UpdateCustomerFunc(ctx, id, name)
434-
}
435-
return a.delegate.UpdateCustomer(ctx, id, name)
438+
if a.UpdateCustomerFunc != nil {
439+
return a.UpdateCustomerFunc(ctx, id, name)
440+
}
441+
return a.delegate.UpdateCustomer(ctx, id, name)
436442
}
437443
```
438444

@@ -441,7 +447,7 @@ In our tests, we can then use the `XXXFunc` field to modify the behaviour of the
441447
```go
442448
failingAPI1 = NewAPI1Decorator(inmemory.NewAPI1())
443449
failingAPI1.UpdateCustomerFunc = func(ctx context.Context, id string, name string) error {
444-
return errors.New("failed to update customer")
450+
return errors.New("failed to update customer")
445451
})
446452
```
447453

@@ -492,17 +498,17 @@ Follow the TDD approach described above to drive out your persistence needs.
492498
package inmemory_test
493499

494500
import (
495-
"github.com/quii/go-fakes-and-contracts/adapters/driven/persistence/inmemory"
496-
"github.com/quii/go-fakes-and-contracts/domain/planner"
497-
"testing"
501+
"github.com/quii/go-fakes-and-contracts/adapters/driven/persistence/inmemory"
502+
"github.com/quii/go-fakes-and-contracts/domain/planner"
503+
"testing"
498504
)
499505

500506
func TestInMemoryPantry(t *testing.T) {
501-
planner.PantryContract{
502-
NewPantry: func() planner.Pantry {
503-
return inmemory.NewPantry()
504-
},
505-
}.Test(t)
507+
planner.PantryContract{
508+
NewPantry: func() planner.Pantry {
509+
return inmemory.NewPantry()
510+
},
511+
}.Test(t)
506512
}
507513

508514
```
@@ -511,24 +517,24 @@ func TestInMemoryPantry(t *testing.T) {
511517
package sqlite_test
512518

513519
import (
514-
"github.com/quii/go-fakes-and-contracts/adapters/driven/persistence/sqlite"
515-
"github.com/quii/go-fakes-and-contracts/domain/planner"
516-
"testing"
520+
"github.com/quii/go-fakes-and-contracts/adapters/driven/persistence/sqlite"
521+
"github.com/quii/go-fakes-and-contracts/domain/planner"
522+
"testing"
517523
)
518524

519525
func TestSQLitePantry(t *testing.T) {
520-
client := sqlite.NewSQLiteClient()
521-
t.Cleanup(func() {
522-
if err := client.Close(); err != nil {
523-
t.Error(err)
524-
}
525-
})
526-
527-
planner.PantryContract{
528-
NewPantry: func() planner.Pantry {
529-
return sqlite.NewPantry(client)
530-
},
531-
}.Test(t)
526+
client := sqlite.NewSQLiteClient()
527+
t.Cleanup(func() {
528+
if err := client.Close(); err != nil {
529+
t.Error(err)
530+
}
531+
})
532+
533+
planner.PantryContract{
534+
NewPantry: func() planner.Pantry {
535+
return sqlite.NewPantry(client)
536+
},
537+
}.Test(t)
532538
}
533539

534540
```

0 commit comments

Comments
 (0)