mock icon indicating copy to clipboard operation
mock copied to clipboard

Changing Expectations When Using Table-Driven Testing

Open jharshman opened this issue 7 years ago • 7 comments

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)
	}

jharshman avatar Nov 17 '18 01:11 jharshman

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

poy avatar Nov 20 '18 06:11 poy

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 avatar Nov 20 '18 17:11 jharshman

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)

		})

	}

jharshman avatar Nov 20 '18 18:11 jharshman

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.

poy avatar Nov 20 '18 18:11 poy

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.

jharshman avatar Nov 20 '18 18:11 jharshman

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

codyoss avatar Oct 18 '19 19:10 codyoss

@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.

hardikmodha avatar Aug 15 '20 07:08 hardikmodha