- Presenting the potato code, part 1
- Using
mockgen
fromgolang/mock
//go:generate
commands for mocks generation- CI check for updated mocks, see
mocks.yml
- Testing
CutAndFry
withgolang/mock
, seepotato_test.go
part 1 - Mock calls order, see
previousCall *gomock.Call
- Comparing with
golang/mock
withmockery
, seepotato_mockery_test.go
- Presenting the potato code, part 2
- 3 ways to subtest, see
potato_test.go
part 2
Created
March 3, 2022 16:57
-
-
Save qdm12/99358e2529a5887ae13a936a7f1c8ae5 to your computer and use it in GitHub Desktop.
Go: mocks and subtests
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
name: Mocks check | |
on: | |
pull_request: | |
branches: | |
- master | |
jobs: | |
mocks-check: | |
runs-on: ubuntu-latest | |
steps: | |
- uses: actions/checkout@v2 | |
- uses: actions/setup-go@v2 | |
with: | |
go-version: "^1.17" | |
- run: go install github.com/golang/mock/mockgen@v1.6.0 | |
- run: go generate -run "mockgen" -tags integration ./... | |
- run: git diff --exit-code |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package potato | |
import "fmt" | |
type Potato uint8 | |
const ( | |
APotato Potato = iota | |
) | |
type Frie struct { | |
Cooked bool | |
} | |
type Cutter interface { | |
Cut(potato Potato) (fries []Frie, err error) | |
} | |
type Fryer interface { | |
Fry(uncookedFries []Frie) (cookedFries []Frie, err error) | |
} | |
func CutAndFry(cutter Cutter, fryer Fryer, potatoes []Potato) (fries []Frie, err error) { | |
for _, potato := range potatoes { | |
uncookedFries, err := cutter.Cut(potato) | |
if err != nil { | |
return nil, fmt.Errorf("cannot cut potato: %w", err) | |
} | |
cookedFries, err := fryer.Fry(uncookedFries) | |
if err != nil { | |
return nil, fmt.Errorf("cannot fry raw fries: %w", err) | |
} | |
fries = append(fries, cookedFries...) | |
} | |
return fries, nil | |
} | |
// ================================== | |
// ================================== | |
// ================================== | |
// Part 2 mocks returning other mocks | |
// ================================== | |
// ================================== | |
// ================================== | |
type Fetcher interface { | |
FetchTools() (cutter Cutter, fryer Fryer, err error) | |
} | |
func MakeFries(fetcher Fetcher, potatoes []Potato) (fries []Frie, err error) { | |
cutter, fryer, err := fetcher.FetchTools() | |
if err != nil { | |
return nil, fmt.Errorf("cannot get our tools: %w", err) | |
} | |
return CutAndFry(cutter, fryer, potatoes) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package potato | |
import ( | |
"errors" | |
"testing" | |
"github.com/stretchr/testify/assert" | |
) | |
//go:generate mockery --name Cutter --case underscore --inpackage --testonly | |
//go:generate mockery --name Fryer --case underscore --inpackage --testonly | |
func Test_CutAndFry(t *testing.T) { | |
errTest := errors.New("test sentinel error") | |
type cutCall struct { | |
potato Potato | |
uncookedFries []Frie | |
err error | |
} | |
type fryCall struct { | |
uncookedFries []Frie | |
cookedFries []Frie | |
err error | |
} | |
testCases := map[string]struct { | |
cutCalls []cutCall | |
fryCalls []fryCall | |
potatoes []Potato | |
expectedFries []Frie | |
expectedErr error | |
expectedErrMessage string | |
}{ | |
"cut error": { | |
cutCalls: []cutCall{ | |
{potato: APotato, err: errTest}, | |
}, | |
potatoes: []Potato{APotato}, | |
expectedErr: errTest, | |
expectedErrMessage: "cannot cut potato: test sentinel error", | |
}, | |
"success for 2 potatoes": { | |
cutCalls: []cutCall{ | |
{ | |
potato: APotato, | |
uncookedFries: []Frie{{Cooked: false}, {Cooked: false}}, | |
}, | |
{ | |
potato: APotato, | |
uncookedFries: []Frie{{Cooked: false}, {Cooked: false}, {Cooked: false}}, | |
}, | |
}, | |
fryCalls: []fryCall{ | |
{ | |
uncookedFries: []Frie{{Cooked: false}, {Cooked: false}}, | |
cookedFries: []Frie{{Cooked: true}, {Cooked: true}}, | |
}, | |
{ | |
uncookedFries: []Frie{{Cooked: false}, {Cooked: false}, {Cooked: false}}, | |
cookedFries: []Frie{{Cooked: true}, {Cooked: true}, {Cooked: true}}, | |
}, | |
}, | |
potatoes: []Potato{APotato, APotato}, | |
expectedFries: []Frie{{Cooked: true}, {Cooked: true}, {Cooked: true}, {Cooked: true}, {Cooked: true}}, | |
}, | |
} | |
for name, testCase := range testCases { | |
t.Run(name, func(t *testing.T) { | |
// Using Mockery: START ====================== | |
cutter := new(MockCutter) | |
for _, call := range testCase.cutCalls { | |
cutter.On("Cut", call.potato).Return(call.uncookedFries, call.err).Once() | |
// - Method call is a string so it's untyped - hard for type changes in codebase | |
// - You need to call .Once() for it to expect it once, imo bad default | |
} | |
fryer := new(MockFryer) | |
for _, call := range testCase.fryCalls { | |
fryer.On("Fry", call.uncookedFries).Return(call.cookedFries, call.err).Once() | |
} | |
// Using Mockery: END ====================== | |
fries, err := CutAndFry(cutter, fryer, testCase.potatoes) | |
assert.Equal(t, testCase.expectedFries, fries) | |
assert.ErrorIs(t, err, testCase.expectedErr) | |
if err != nil { | |
assert.EqualError(t, err, testCase.expectedErrMessage) | |
} | |
// Mockery additional step required (EASY TO FORGET) | |
cutter.AssertExpectations(t) | |
fryer.AssertExpectations(t) | |
}) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package potato | |
import ( | |
"errors" | |
"testing" | |
"github.com/golang/mock/gomock" | |
"github.com/stretchr/testify/assert" | |
) | |
//go:generate mockgen -destination=mock_cutter_test.go -package $GOPACKAGE . Cutter | |
//go:generate mockgen -destination=mock_fryer_test.go -package $GOPACKAGE . Fryer | |
func Test_CutAndFry(t *testing.T) { | |
errTest := errors.New("test sentinel error") | |
type cutCall struct { | |
potato Potato | |
uncookedFries []Frie | |
err error | |
} | |
type fryCall struct { | |
uncookedFries []Frie | |
cookedFries []Frie | |
err error | |
} | |
testCases := map[string]struct { | |
cutCalls []cutCall | |
fryCalls []fryCall | |
potatoes []Potato | |
expectedFries []Frie | |
expectedErr error | |
expectedErrMessage string | |
}{ | |
"cut error": { | |
cutCalls: []cutCall{ | |
{potato: APotato, err: errTest}, | |
}, | |
potatoes: []Potato{APotato}, | |
expectedErr: errTest, | |
expectedErrMessage: "cannot cut potato: test sentinel error", | |
}, | |
"success for 2 potatoes": { | |
cutCalls: []cutCall{ | |
{ | |
potato: APotato, | |
uncookedFries: []Frie{{Cooked: false}, {Cooked: false}}, | |
}, | |
{ | |
potato: APotato, | |
uncookedFries: []Frie{{Cooked: false}, {Cooked: false}, {Cooked: false}}, | |
}, | |
}, | |
fryCalls: []fryCall{ | |
{ | |
uncookedFries: []Frie{{Cooked: false}, {Cooked: false}}, | |
cookedFries: []Frie{{Cooked: true}, {Cooked: true}}, | |
}, | |
{ | |
uncookedFries: []Frie{{Cooked: false}, {Cooked: false}, {Cooked: false}}, | |
cookedFries: []Frie{{Cooked: true}, {Cooked: true}, {Cooked: true}}, | |
}, | |
}, | |
potatoes: []Potato{APotato, APotato}, | |
expectedFries: []Frie{{Cooked: true}, {Cooked: true}, {Cooked: true}, {Cooked: true}, {Cooked: true}}, | |
}, | |
} | |
for name, testCase := range testCases { | |
t.Run(name, func(t *testing.T) { | |
ctrl := gomock.NewController(t) | |
cutter := NewMockCutter(ctrl) | |
var previousCall *gomock.Call | |
for _, call := range testCase.cutCalls { | |
newCall := cutter.EXPECT().Cut(call.potato). | |
Return(call.uncookedFries, call.err) | |
if previousCall != nil { | |
newCall.After(previousCall) | |
} | |
previousCall = newCall | |
} | |
fryer := NewMockFryer(ctrl) | |
for _, call := range testCase.fryCalls { | |
fryer.EXPECT().Fry(call.uncookedFries). | |
Return(call.cookedFries, call.err) | |
} | |
fries, err := CutAndFry(cutter, fryer, testCase.potatoes) | |
assert.Equal(t, testCase.expectedFries, fries) | |
assert.ErrorIs(t, err, testCase.expectedErr) | |
if err != nil { | |
assert.EqualError(t, err, testCase.expectedErrMessage) | |
} | |
}) | |
} | |
} | |
//go:generate mockgen -destination=mock_fetcher_test.go -package $GOPACKAGE . Fetcher | |
// ================================== | |
// ================================== | |
// ================================== | |
// Part 2 mocks returning other mocks | |
// ================================== | |
// ================================== | |
// ================================== | |
func Test_MakeFries_Table(t *testing.T) { | |
errTest := errors.New("test sentinel error") | |
type cutCall struct { | |
potato Potato | |
uncookedFries []Frie | |
err error | |
} | |
type fryCall struct { | |
uncookedFries []Frie | |
cookedFries []Frie | |
err error | |
} | |
testCases := map[string]struct { | |
cutCalls []cutCall | |
fryCalls []fryCall | |
fetchErr error // NEW | |
potatoes []Potato | |
expectedFries []Frie | |
expectedErr error | |
expectedErrMessage string | |
}{ | |
"fetch error": { | |
fetchErr: errTest, | |
expectedErr: errTest, | |
expectedErrMessage: "cannot get our tools: test sentinel error", | |
}, | |
"success for one potato": { | |
cutCalls: []cutCall{ | |
{ | |
potato: APotato, | |
uncookedFries: []Frie{{Cooked: false}, {Cooked: false}}, | |
}, | |
}, | |
fryCalls: []fryCall{ | |
{ | |
uncookedFries: []Frie{{Cooked: false}, {Cooked: false}}, | |
cookedFries: []Frie{{Cooked: true}, {Cooked: true}}, | |
}, | |
}, | |
potatoes: []Potato{APotato}, | |
expectedFries: []Frie{{Cooked: true}, {Cooked: true}}}, | |
} | |
for name, testCase := range testCases { | |
t.Run(name, func(t *testing.T) { | |
ctrl := gomock.NewController(t) | |
cutter := NewMockCutter(ctrl) | |
for _, call := range testCase.cutCalls { | |
cutter.EXPECT().Cut(call.potato).Return(call.uncookedFries, call.err) | |
} | |
fryer := NewMockFryer(ctrl) | |
for _, call := range testCase.fryCalls { | |
fryer.EXPECT().Fry(call.uncookedFries).Return(call.cookedFries, call.err) | |
} | |
fetcher := NewMockFetcher(ctrl) | |
fetcher.EXPECT().FetchTools(). | |
Return(cutter, fryer, testCase.fetchErr) | |
fries, err := MakeFries(fetcher, testCase.potatoes) | |
assert.Equal(t, testCase.expectedFries, fries) | |
assert.ErrorIs(t, err, testCase.expectedErr) | |
if err != nil { | |
assert.EqualError(t, err, testCase.expectedErrMessage) | |
} | |
}) | |
} | |
} | |
func Test_MakeFries_SimpleSubtests(t *testing.T) { | |
errTest := errors.New("test sentinel error") | |
t.Run("fetch error", func(t *testing.T) { | |
ctrl := gomock.NewController(t) | |
fetcher := NewMockFetcher(ctrl) | |
fetcher.EXPECT().FetchTools(). | |
Return(nil, nil, errTest) | |
fries, err := MakeFries(fetcher, nil) | |
assert.Nil(t, fries) | |
assert.ErrorIs(t, err, errTest) | |
if err != nil { | |
assert.EqualError(t, err, "cannot get our tools: test sentinel error") | |
} | |
}) | |
t.Run("success for one potato", func(t *testing.T) { | |
ctrl := gomock.NewController(t) | |
cutter := NewMockCutter(ctrl) | |
cutter.EXPECT().Cut(APotato). | |
Return([]Frie{{Cooked: false}, {Cooked: false}}, nil) | |
fryer := NewMockFryer(ctrl) | |
fryer.EXPECT().Fry([]Frie{{Cooked: false}, {Cooked: false}}). | |
Return([]Frie{{Cooked: true}, {Cooked: true}}, nil) | |
fetcher := NewMockFetcher(ctrl) | |
fetcher.EXPECT().FetchTools(). | |
Return(cutter, fryer, nil) | |
fries, err := MakeFries(fetcher, []Potato{APotato}) | |
expectedFries := []Frie{{Cooked: true}, {Cooked: true}} | |
assert.Equal(t, expectedFries, fries) | |
assert.NoError(t, err) | |
}) | |
} | |
func Test_MakeFries_Table_Functions(t *testing.T) { | |
errTest := errors.New("test sentinel error") | |
testCases := map[string]struct { | |
cutterBuilder func(ctrl *gomock.Controller) Cutter | |
fryerBuilder func(ctrl *gomock.Controller) Fryer | |
fetcherBuilder func(ctrl *gomock.Controller, cutter Cutter, fryer Fryer) Fetcher | |
potatoes []Potato | |
expectedFries []Frie | |
expectedErr error | |
expectedErrMessage string | |
}{ | |
"fetch error": { | |
cutterBuilder: func(ctrl *gomock.Controller) Cutter { return nil }, | |
fryerBuilder: func(ctrl *gomock.Controller) Fryer { return nil }, | |
fetcherBuilder: func(ctrl *gomock.Controller, cutter Cutter, fryer Fryer) Fetcher { | |
fetcher := NewMockFetcher(ctrl) | |
fetcher.EXPECT().FetchTools().Return(nil, nil, errTest) | |
return fetcher | |
}, | |
expectedErr: errTest, | |
expectedErrMessage: "cannot get our tools: test sentinel error", | |
}, | |
"success for 1 potato": { | |
cutterBuilder: func(ctrl *gomock.Controller) Cutter { | |
cutter := NewMockCutter(ctrl) | |
cutter.EXPECT().Cut(APotato). | |
Return([]Frie{{Cooked: false}, {Cooked: false}}, nil) | |
return cutter | |
}, | |
fryerBuilder: func(ctrl *gomock.Controller) Fryer { | |
fryer := NewMockFryer(ctrl) | |
fryer.EXPECT().Fry([]Frie{{Cooked: false}, {Cooked: false}}). | |
Return([]Frie{{Cooked: true}, {Cooked: true}}, nil) | |
return fryer | |
}, | |
fetcherBuilder: func(ctrl *gomock.Controller, cutter Cutter, fryer Fryer) Fetcher { | |
fetcher := NewMockFetcher(ctrl) | |
fetcher.EXPECT().FetchTools().Return(cutter, fryer, nil) | |
return fetcher | |
}, | |
potatoes: []Potato{APotato}, | |
expectedFries: []Frie{{Cooked: true}, {Cooked: true}}}, | |
} | |
for name, testCase := range testCases { | |
t.Run(name, func(t *testing.T) { | |
ctrl := gomock.NewController(t) | |
cutter := testCase.cutterBuilder(ctrl) | |
fryer := testCase.fryerBuilder(ctrl) | |
fetcher := testCase.fetcherBuilder(ctrl, cutter, fryer) | |
fries, err := MakeFries(fetcher, testCase.potatoes) | |
assert.Equal(t, testCase.expectedFries, fries) | |
assert.ErrorIs(t, err, testCase.expectedErr) | |
if err != nil { | |
assert.EqualError(t, err, testCase.expectedErrMessage) | |
} | |
}) | |
} | |
} | |
func bigFunc(n int, f func(n int) int) int { | |
x := f(n) | |
return x * 2 | |
} | |
func Test_bigFunc(t *testing.T) { | |
timesCalled := 0 | |
f := func(n int) int { | |
timesCalled++ | |
return 1 | |
} | |
x := bigFunc(2, f) | |
assert.Equal(t, 4, x) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment