go.uber.org/yarpc@v1.72.1/encoding/thrift/thriftrw-plugin-yarpc/gomock_test.go (about)

     1  // Copyright (c) 2022 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  package main
    22  
    23  import (
    24  	"context"
    25  	"errors"
    26  	"strings"
    27  	"testing"
    28  
    29  	"github.com/golang/mock/gomock"
    30  	"github.com/stretchr/testify/assert"
    31  	"go.uber.org/thriftrw/ptr"
    32  	"go.uber.org/yarpc"
    33  	"go.uber.org/yarpc/encoding/thrift/thriftrw-plugin-yarpc/internal/tests/atomic/readonlystoretest"
    34  	"go.uber.org/yarpc/encoding/thrift/thriftrw-plugin-yarpc/internal/tests/atomic/storetest"
    35  	"go.uber.org/yarpc/encoding/thrift/thriftrw-plugin-yarpc/internal/tests/common/baseservicetest"
    36  	"go.uber.org/yarpc/encoding/thrift/thriftrw-plugin-yarpc/internal/tests/common/emptyservicetest"
    37  	"go.uber.org/yarpc/encoding/thrift/thriftrw-plugin-yarpc/internal/tests/common/extendemptytest"
    38  	"go.uber.org/yarpc/encoding/thrift/thriftrw-plugin-yarpc/internal/tests/common/extendonlytest"
    39  )
    40  
    41  func TestMockClients(t *testing.T) {
    42  	ctx := context.Background()
    43  
    44  	tests := []struct {
    45  		desc           string
    46  		withController func(*gomock.Controller)
    47  
    48  		wantStatus     FakeTestStatus
    49  		wantPanic      interface{}
    50  		wantErrorsLike []string
    51  	}{
    52  		{
    53  			desc: "empty",
    54  			withController: func(ctrl *gomock.Controller) {
    55  				emptyservicetest.NewMockClient(ctrl).EXPECT()
    56  			},
    57  		},
    58  		{
    59  			desc: "extends empty: unexpected",
    60  			withController: func(ctrl *gomock.Controller) {
    61  				c := extendemptytest.NewMockClient(ctrl)
    62  				c.Hello(ctx)
    63  			},
    64  			wantStatus:     Fatal,
    65  			wantErrorsLike: []string{"Unexpected call"},
    66  		},
    67  		{
    68  			desc: "extends empty: expected:",
    69  			withController: func(ctrl *gomock.Controller) {
    70  				c := extendemptytest.NewMockClient(ctrl)
    71  				c.EXPECT().Hello(gomock.Any()).Return(nil)
    72  				assert.NoError(t, c.Hello(ctx))
    73  			},
    74  		},
    75  		{
    76  			desc: "extends empty: missing",
    77  			withController: func(ctrl *gomock.Controller) {
    78  				c := extendemptytest.NewMockClient(ctrl)
    79  				c.EXPECT().Hello(gomock.Any()).Return(nil)
    80  			},
    81  			wantStatus: Fatal,
    82  			wantErrorsLike: []string{
    83  				"missing call(s) to [*extendemptytest.MockClient.Hello(is anything)",
    84  				"aborting test due to missing call(s)",
    85  			},
    86  		},
    87  		{
    88  			desc: "extends only: unexpected",
    89  			withController: func(ctrl *gomock.Controller) {
    90  				c := extendonlytest.NewMockClient(ctrl)
    91  				c.Healthy(ctx)
    92  			},
    93  			wantStatus:     Fatal,
    94  			wantErrorsLike: []string{"Unexpected call"},
    95  		},
    96  		{
    97  			desc: "extends only: expected:",
    98  			withController: func(ctrl *gomock.Controller) {
    99  				c := extendonlytest.NewMockClient(ctrl)
   100  				c.EXPECT().Healthy(gomock.Any()).Return(true, nil)
   101  				healthy, err := c.Healthy(ctx)
   102  				assert.NoError(t, err)
   103  				assert.True(t, healthy)
   104  			},
   105  		},
   106  		{
   107  			desc: "extends only: missing",
   108  			withController: func(ctrl *gomock.Controller) {
   109  				c := extendonlytest.NewMockClient(ctrl)
   110  				c.EXPECT().Healthy(gomock.Any()).Return(false, nil)
   111  			},
   112  			wantStatus: Fatal,
   113  			wantErrorsLike: []string{
   114  				"missing call(s) to [*extendonlytest.MockClient.Healthy(is anything)",
   115  				"aborting test due to missing call(s)",
   116  			},
   117  		},
   118  		{
   119  			desc: "base: expected with options",
   120  			withController: func(ctrl *gomock.Controller) {
   121  				c := baseservicetest.NewMockClient(ctrl)
   122  				c.EXPECT().Healthy(gomock.Any(), gomock.Any()).Return(true, nil)
   123  
   124  				// NOTE: Each option has to have a different argument in the
   125  				// EXPECT call. We should figure out an API to add assertions
   126  				// on options. See https://github.com/yarpc/yarpc-go/issues/683
   127  
   128  				healthy, err := c.Healthy(ctx, yarpc.WithHeader("key", "value"))
   129  				assert.True(t, healthy)
   130  				assert.NoError(t, err)
   131  			},
   132  		},
   133  		{
   134  			desc: "store: healthy: unexpected call",
   135  			withController: func(ctrl *gomock.Controller) {
   136  				c := storetest.NewMockClient(ctrl)
   137  				c.Healthy(ctx)
   138  			},
   139  			wantStatus:     Fatal,
   140  			wantErrorsLike: []string{"Unexpected call"},
   141  		},
   142  		{
   143  			desc: "store: healthy: expected call",
   144  			withController: func(ctrl *gomock.Controller) {
   145  				c := storetest.NewMockClient(ctrl)
   146  				c.EXPECT().Healthy(gomock.Any()).Return(true, nil)
   147  
   148  				result, err := c.Healthy(ctx)
   149  				assert.NoError(t, err, "mock should return no error")
   150  				assert.True(t, result, "mock should return true")
   151  			},
   152  		},
   153  		{
   154  			desc: "store: healthy: expected call: throw error",
   155  			withController: func(ctrl *gomock.Controller) {
   156  				c := storetest.NewMockClient(ctrl)
   157  				c.EXPECT().Healthy(gomock.Any()).Return(false, errors.New("great sadness"))
   158  
   159  				_, err := c.Healthy(ctx)
   160  				assert.Equal(t, errors.New("great sadness"), err)
   161  			},
   162  		},
   163  		{
   164  			desc: "readonly store: integer: missing",
   165  			withController: func(ctrl *gomock.Controller) {
   166  				c := readonlystoretest.NewMockClient(ctrl)
   167  				c.EXPECT().Integer(gomock.Any(), ptr.String("foo")).Return(int64(42), nil)
   168  			},
   169  			wantStatus: Fatal,
   170  			wantErrorsLike: []string{
   171  				"missing call(s) to [*readonlystoretest.MockClient.Integer(is anything",
   172  				"aborting test due to missing call(s)",
   173  			},
   174  		},
   175  		{
   176  			desc: "store: integer: expected",
   177  			withController: func(ctrl *gomock.Controller) {
   178  				c := storetest.NewMockClient(ctrl)
   179  				c.EXPECT().Integer(gomock.Any(), ptr.String("foo")).Return(int64(42), nil)
   180  				result, err := c.Integer(ctx, ptr.String("foo"))
   181  				assert.NoError(t, err)
   182  				assert.Equal(t, int64(42), result)
   183  			},
   184  		},
   185  		{
   186  			desc: "store: forget: unexpected",
   187  			withController: func(ctrl *gomock.Controller) {
   188  				c := storetest.NewMockClient(ctrl)
   189  				c.Forget(ctx, ptr.String("hello"))
   190  			},
   191  			wantStatus:     Fatal,
   192  			wantErrorsLike: []string{"Unexpected call"},
   193  		},
   194  		{
   195  			desc: "store: forget: expected",
   196  			withController: func(ctrl *gomock.Controller) {
   197  				c := storetest.NewMockClient(ctrl)
   198  				c.EXPECT().Forget(gomock.Any(), ptr.String("hello")).Return(nil, errors.New("great sadness"))
   199  
   200  				_, err := c.Forget(ctx, ptr.String("hello"))
   201  				assert.Equal(t, errors.New("great sadness"), err)
   202  			},
   203  		},
   204  		{
   205  			desc: "store: forget: missing",
   206  			withController: func(ctrl *gomock.Controller) {
   207  				c := storetest.NewMockClient(ctrl)
   208  				c.EXPECT().Forget(gomock.Any(), ptr.String("hello")).Return(nil, errors.New("great sadness"))
   209  			},
   210  			wantStatus: Fatal,
   211  			wantErrorsLike: []string{
   212  				"missing call(s) to [*storetest.MockClient.Forget(is anything",
   213  				"aborting test due to missing call(s)",
   214  			},
   215  		},
   216  	}
   217  
   218  	for _, tt := range tests {
   219  		t.Run(tt.desc, func(t *testing.T) {
   220  			result := withFakeTestReporter(func(fakeT gomock.TestReporter) {
   221  				mockCtrl := gomock.NewController(fakeT)
   222  				defer mockCtrl.Finish()
   223  
   224  				tt.withController(mockCtrl)
   225  			})
   226  
   227  			if !assert.Equal(t, tt.wantStatus, result.Status, "expected %v, got %v", tt.wantStatus, result.Status) {
   228  				if result.Status == Panicked {
   229  					t.Fatalf("panicked: %v\n%v", result.Panic, result.PanicTrace)
   230  				}
   231  			}
   232  
   233  			assert.Equal(t, tt.wantPanic, result.Panic)
   234  			assertErrorsMatch(t, tt.wantErrorsLike, result.Errors)
   235  		})
   236  	}
   237  }
   238  
   239  func assertErrorsMatch(t *testing.T, wantErrorsLike, errors []string) {
   240  	var unexpectedErrors []string
   241  	for _, err := range errors {
   242  		before := len(wantErrorsLike)
   243  		wantErrorsLike = removeMatchingError(wantErrorsLike, err)
   244  		if len(wantErrorsLike) == before {
   245  			unexpectedErrors = append(unexpectedErrors, err)
   246  			continue
   247  		}
   248  	}
   249  
   250  	if len(wantErrorsLike) > 0 {
   251  		msg := "expected but did not receive errors like:"
   252  		for _, m := range wantErrorsLike {
   253  			msg += "\n -  " + indentTail(4, m)
   254  		}
   255  		t.Errorf(msg)
   256  	}
   257  
   258  	if len(unexpectedErrors) > 0 {
   259  		msg := "received unexpected errors:"
   260  		for _, err := range unexpectedErrors {
   261  			msg += "\n -  " + indentTail(4, err)
   262  		}
   263  		t.Error(msg)
   264  	}
   265  }
   266  
   267  func removeMatchingError(matchers []string, err string) []string {
   268  	match := -1
   269  	for i, m := range matchers {
   270  		if strings.Contains(err, m) {
   271  			match = i
   272  			break
   273  		}
   274  	}
   275  
   276  	if match < 0 {
   277  		return matchers
   278  	}
   279  
   280  	matchers = append(matchers[:match], matchers[match+1:]...)
   281  	return matchers
   282  }
   283  
   284  // indentTail prepends the given number of spaces to all lines following the
   285  // first line of the given string.
   286  func indentTail(spaces int, s string) string {
   287  	prefix := strings.Repeat(" ", spaces)
   288  	lines := strings.Split(s, "\n")
   289  	for i, line := range lines[1:] {
   290  		lines[i+1] = prefix + line
   291  	}
   292  	return strings.Join(lines, "\n")
   293  }