golang / mock

GoMock is a mocking framework for the Go programming language.
Apache License 2.0
9.32k stars 607 forks source link

Changing Expectations When Using Table-Driven Testing #223

Open jharshman opened 6 years ago

jharshman commented 6 years ago

I've been trying to use the gomock to perform some table driven testing. I need to be able to change the expected return values based on permutations of different errors and events.

But what I've found is that when I try to do that, the code fails in unexpected ways. Is this expected behavior? Is the intended usage to create a new mock controller for each expectation? This could be accomplished in sub-tests but seems very repetitive.

Example:

func TestDeployRelease(t *testing.T) {
        ... [ snip ] ...

    mockCtrl := gomock.NewController(t)
    defer mockCtrl.Finish()

    mockedTiller := mock.NewMockInterface(mockCtrl)
    mockedStore := mock.NewMockDataStore(mockCtrl)
    mockedQuay := mock.NewMockDataStore(mockCtrl)

         ... [ snip ] ...

    installTests := []struct {
                ... [ snip ] ...
    }{
        /* successful */
        {200, &services.GetHistoryResponse{nil}, errors.New("not found"), nil, nil, &services.InstallReleaseResponse{Release: &release.Release{Name: "foo", Version: 1, Namespace: "bar"}}, nil},

        /* install error */
        {500, &services.GetHistoryResponse{nil}, errors.New("not found"), nil, nil, &services.InstallReleaseResponse{Release: &release.Release{Name: "foo", Version: 1, Namespace: "bar"}}, errors.New("some error")},

        ... [ snip ] ...
    }

    for k, v := range installTests {
        mockedStore.EXPECT().Get("", gomock.Any()).Return(datastoreResponse(), v.storeErr)
        mockedQuay.EXPECT().Get("", gomock.Any()).Return(ChartBytes(), v.quayErr)
        mockedTiller.EXPECT().ReleaseHistory(gomock.Any(), gomock.Any()).Return(v.hist, v.histErr)
        mockedTiller.EXPECT().InstallReleaseFromChart(gomock.Any(), gomock.Any(), gomock.Any()).Return(v.inst, v.instErr)

        req, _ := http.NewRequest("POST", "/release", bytes.NewBuffer(payload()))
        w := httptest.NewRecorder()
        srv.ServeHTTP(w, req)
        is.Equal(w.Code, v.code)
    }
poy commented 6 years ago

I would say sub-tests are the right way to go. Take a look at T.Run() if you haven't already.

jharshman commented 6 years ago

Unfortunately that doesn't resolve the repetitiveness of setting up the mock controller every single iteration of the table. I think the root of the problem here is that one can't dynamically change the return values of the mocked function. Therefore, once it's set on the first iteration, you cannot re-set it on a second pass.

jharshman commented 6 years ago

Setting up the mock controller & expectation each iteration. The best way I have found to do it is in a loop like so:

    for _, v := range tests {

        t.Run(v.n, func(t *testing.T) {
            mockCtrl := gomock.NewController(t)
            defer mockCtrl.Finish()
            mockedTiller := mock.NewMockInterface(mockCtrl)
            srv.tiller = mockedTiller

            mockedTiller.EXPECT().ListReleases(gomock.Any()).Return(v.r, v.e).Times(1)

            w := httptest.NewRecorder()
            srv.ServeHTTP(w, req)
            is.Equal(w.Code, v.c)

        })

    }
poy commented 6 years ago

I like that pattern well enough and have seen variations of it in other repos.

I don't think gomock should really change anything but it might be worth documenting your for loop in the README and/or sample.

jharshman commented 6 years ago

I agree, I've seen others have the same question on why they can't re-set the expectation. I'll look into adding some sample code.

codyoss commented 5 years ago

@jharshman you still interested in working on this by chance?

hardikmodha commented 4 years ago

@jharshman I've opened https://github.com/golang/mock/issues/459 to discuss similar requirements when using go-mock. You can add your valuable inputs there.