Best Practices v3.7.0
INFO
Minimum Requirements: Go 1.26 or later
Version history: Embedded SDK introduced in v3.7.0. Current API since v3.16.0; the legacy
sdk.Run/mock.Stub/mock.VerifyAPI was removed in v3.20.0. See the Upgrade Guide.
Recommended patterns and practices for using GripMock Embedded SDK.
Test Helper Functions
Create reusable helper functions for common mock setup:
func runMyServiceMock(t *testing.T, opts ...sdk.Option) *sdk.Server {
t.Helper()
// ARRANGE
// Add default options
allOpts := []sdk.Option{
sdk.WithFileDescriptor(service.File_service_proto),
}
allOpts = append(allOpts, opts...)
return sdk.NewServer(t, allOpts...)
}
func TestMyService_WithHelper(t *testing.T) {
// ARRANGE
srv := runMyServiceMock(t)
// Define stubs in the Arrange phase
srv.ExpectUnary(MyService_MyMethod_FullMethodName).
Match("id", "test").
Return("result", "success")
client := NewMyServiceClient(srv.Conn())
// ACT
resp, err := client.MyMethod(t.Context(), &MyRequest{Id: "test"})
// ASSERT
require.NoError(t, err)
require.Equal(t, "success", resp.Result)
}Parallel Tests
Use sessions for parallel tests when using remote mode:
func TestMyService_Parallel(t *testing.T) {
t.Parallel()
// ARRANGE
srv := sdk.NewServer(t,
sdk.WithRemote("localhost:4770", "http://localhost:4771"),
sdk.WithFileDescriptor(service.File_service_proto),
sdk.WithSession(t.Name()), // Isolate this test's stubs
)
srv.ExpectUnary(MyService_MyMethod_FullMethodName).
Match("id", "parallel").
Return("result", "parallel-success")
client := NewMyServiceClient(srv.Conn())
// ACT
resp, err := client.MyMethod(t.Context(), &MyRequest{Id: "parallel"})
// ASSERT
require.NoError(t, err)
require.Equal(t, "parallel-success", resp.Result)
}Proper Cleanup
Always pass t to NewServer. The SDK registers cleanup automatically and verifies Times(...) expectations:
func TestCleanupIsAutomatic(t *testing.T) {
// ARRANGE
srv := sdk.NewServer(t, sdk.WithFileDescriptor(service.File_service_proto))
srv.ExpectUnary(MyService_MyMethod_FullMethodName).
Match("id", "manual").
Return("result", "manual-success")
client := NewMyServiceClient(srv.Conn())
// ACT
resp, err := client.MyMethod(t.Context(), &MyRequest{Id: "manual"})
// ASSERT
require.NoError(t, err)
require.Equal(t, "manual-success", resp.Result)
// No explicit srv.Close() is required in tests.
}Verify Expected Calls
Always verify that your code makes the expected calls:
func TestMyService_WithVerification(t *testing.T) {
// ARRANGE
srv := sdk.NewServer(t, sdk.WithFileDescriptor(service.File_service_proto))
srv.ExpectUnary(MyService_MyMethod_FullMethodName).
Match("id", "test").
Return("result", "success")
client := NewMyServiceClient(srv.Conn())
// ACT
_, _ = client.MyMethod(t.Context(), &MyRequest{Id: "test"})
_, _ = client.MyMethod(t.Context(), &MyRequest{Id: "test"})
// ASSERT - Verify the expected call was made exactly 2 times
require.Equal(t, 2, srv.Called(MyService_MyMethod_FullMethodName))
}One Stub per Scenario
Give each branch its own stub, matched on the field that distinguishes it, rather than one stub with conditional logic:
func TestUserService_ComplexScenario(t *testing.T) {
// ARRANGE
srv := sdk.NewServer(t, sdk.WithFileDescriptor(user.File_user_service_proto))
// Existing user stub
srv.ExpectUnary(UserService_GetUser_FullMethodName).
Match("id", "existing-user").
Return("name", "John Doe", "email", "john@example.com")
// Missing user stub
srv.ExpectUnary(UserService_GetUser_FullMethodName).
Match("id", "missing-user").
ReturnError(codes.NotFound, "User not found")
client := NewUserServiceClient(srv.Conn())
// ACT
existingResp, _ := client.GetUser(t.Context(), &GetUserRequest{Id: "existing-user"})
_, missingErr := client.GetUser(t.Context(), &GetUserRequest{Id: "missing-user"})
// ASSERT
require.Equal(t, "John Doe", existingResp.GetName())
require.Equal(t, "john@example.com", existingResp.GetEmail())
require.Error(t, missingErr)
require.Equal(t, codes.NotFound, status.Code(missingErr))
}Startup Failures
sdk.NewServer returns no error — a bad descriptor or an unavailable port panics, which fails the test at the line that caused it. There is nothing to check for.
Use Times for Exact Call Verification
When you need to verify exact call counts, use the Times feature:
func TestRetryLogic_WithTimes(t *testing.T) {
// ARRANGE
srv := sdk.NewServer(t, sdk.WithFileDescriptor(service.File_service_proto))
// First 2 calls fail, 3rd succeeds (simulating retry logic)
srv.ExpectUnary(ExternalService_Call_FullMethodName).
Match("attempt", "fail").
Times(2). // Allow this stub to match exactly 2 times
ReturnError(codes.Unavailable, "Service unavailable")
srv.ExpectUnary(ExternalService_Call_FullMethodName).
Match("attempt", "success").
Return("result", "success")
client := NewExternalServiceClient(srv.Conn())
// ACT
// First two calls will fail (triggering retries)
_, err1 := client.Call(t.Context(), &CallRequest{Attempt: "fail"})
_, err2 := client.Call(t.Context(), &CallRequest{Attempt: "fail"})
// Third call will succeed
successResp, err3 := client.Call(t.Context(), &CallRequest{Attempt: "success"})
// ASSERT
require.Error(t, err1)
require.Error(t, err2)
require.NoError(t, err3)
require.Equal(t, "success", successResp.GetResult())
// Verification happens automatically due to Times(2) and passing t to NewServer
}Avoid Over-Mocking
Only mock what you need to test:
func TestPaymentService_WithMinimalMocks(t *testing.T) {
// ARRANGE
srv := sdk.NewServer(t, sdk.WithFileDescriptor(payment.File_payment_service_proto))
// Good: Only mock the service you're testing
srv.ExpectUnary(PaymentService_Charge_FullMethodName).
Match("amount", 100).
Return("transactionId", "tx-123")
client := NewPaymentServiceClient(srv.Conn())
// ACT
resp, err := client.Charge(t.Context(), &ChargeRequest{Amount: 100})
// ASSERT
require.NoError(t, err)
require.Equal(t, "tx-123", resp.GetTransactionId())
}Everything Together
The patterns above in one test:
func TestOrderService_Comprehensive(t *testing.T) {
// ARRANGE
srv := sdk.NewServer(t, sdk.WithFileDescriptor(order.File_order_service_proto))
// Setup multiple stubs with different behaviors
srv.ExpectUnary(OrderService_CreateOrder_FullMethodName).
Match("userId", "premium").
Return("orderId", "ORD-001", "status", "created")
srv.ExpectUnary(OrderService_GetOrder_FullMethodName).
Match("orderId", "ORD-001").
Times(2). // Expected to be called exactly 2 times
Return("status", "processing", "total", 99.99)
srv.ExpectUnary(OrderService_CancelOrder_FullMethodName).
Match("orderId", "ORD-001").
Return("status", "cancelled")
client := NewOrderServiceClient(srv.Conn())
// ACT
// Create an order
createResp, err := client.CreateOrder(t.Context(), &CreateOrderRequest{UserId: "premium"})
require.NoError(t, err)
// Check order status twice
status1, err := client.GetOrder(t.Context(), &GetOrderRequest{OrderId: "ORD-001"})
require.NoError(t, err)
status2, err := client.GetOrder(t.Context(), &GetOrderRequest{OrderId: "ORD-001"})
require.NoError(t, err)
// Cancel the order
cancelResp, err := client.CancelOrder(t.Context(), &CancelOrderRequest{OrderId: "ORD-001"})
require.NoError(t, err)
// ASSERT
require.Equal(t, "ORD-001", createResp.GetOrderId())
require.Equal(t, "created", createResp.GetStatus())
require.Equal(t, "processing", status1.GetStatus())
require.Equal(t, 99.99, status1.GetTotal())
require.Equal(t, "processing", status2.GetStatus())
require.Equal(t, 99.99, status2.GetTotal())
require.Equal(t, "cancelled", cancelResp.GetStatus())
// Verify call counts
require.Equal(t, 1, srv.Called(OrderService_CreateOrder_FullMethodName))
require.Equal(t, 2, srv.Called(OrderService_GetOrder_FullMethodName)) // Due to Times(2)
require.Equal(t, 1, srv.Called(OrderService_CancelOrder_FullMethodName))
}