Skip to content

Instantly share code, notes, and snippets.

@qdm12
Created March 3, 2022 16:57
Show Gist options
  • Select an option

  • Save qdm12/99358e2529a5887ae13a936a7f1c8ae5 to your computer and use it in GitHub Desktop.

Select an option

Save qdm12/99358e2529a5887ae13a936a7f1c8ae5 to your computer and use it in GitHub Desktop.

Revisions

  1. qdm12 created this gist Mar 3, 2022.
    11 changes: 11 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,11 @@
    # Contents

    1. Presenting the potato code, part 1
    2. Using `mockgen` from `golang/mock`
    3. `//go:generate` commands for mocks generation
    4. CI check for updated mocks, see `mocks.yml`
    5. Testing `CutAndFry` with `golang/mock`, see `potato_test.go` part 1
    6. Mock calls order, see `previousCall *gomock.Call`
    7. Comparing with `golang/mock` with `mockery`, see `potato_mockery_test.go`
    8. Presenting the potato code, part 2
    9. 3 ways to subtest, see `potato_test.go` part 2
    21 changes: 21 additions & 0 deletions mocks.yml
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,21 @@
    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/[email protected]

    - run: go generate -run "mockgen" -tags integration ./...

    - run: git diff --exit-code
    60 changes: 60 additions & 0 deletions potato.go
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,60 @@
    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)
    }
    99 changes: 99 additions & 0 deletions potato_mockery_test.go
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,99 @@
    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)
    })
    }
    }
    307 changes: 307 additions & 0 deletions potato_test.go
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,307 @@
    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)
    }