Remote Mode v3.16.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.
Connect to a remote GripMock instance instead of running embedded. When using remote mode, you must provide both the gRPC endpoint (for mock server) and HTTP endpoint (for management operations).
Use sdk.WithRemote(grpcAddr, restURL) for remote mode.
WARNING
Remote mode works without sdk.WithSession(...), but this is not recommended for tests. Without sessions, stubs and history can leak between tests and cause flaky behavior.
Connecting to Remote GripMock
When connecting to a remote GripMock instance, you must specify both the gRPC endpoint (for the mock server) and the HTTP endpoint (for management operations):
func TestMyService_Remote(t *testing.T) {
// ARRANGE
// Connect to a remote GripMock server - specify both gRPC and HTTP endpoints
srv := sdk.NewServer(t,
sdk.WithRemote("localhost:4770", "http://localhost:4771"), // gRPC endpoint, HTTP management endpoint
sdk.WithFileDescriptor(service.File_service_proto),
)
// Define stubs in the Arrange phase
srv.ExpectUnary(MyService_MyMethod_FullMethodName).
Match("id", "remote-test").
Return("result", "from-remote")
client := NewMyServiceClient(srv.Conn())
// ACT
resp, err := client.MyMethod(t.Context(), &MyRequest{Id: "remote-test"})
// ASSERT
require.NoError(t, err)
require.Equal(t, "from-remote", resp.Result)
}Session Isolation
func TestMyService_SessionIsolation(t *testing.T) {
t.Parallel() // Safe with sessions
// ARRANGE
// Use a unique session for this test
srv := sdk.NewServer(t,
sdk.WithRemote("localhost:4770", "http://localhost:4771"),
sdk.WithFileDescriptor(service.File_service_proto),
sdk.WithSession(t.Name()), // Use test name as session ID
)
// Stubs in this session are isolated from other tests
srv.ExpectUnary(MyService_MyMethod_FullMethodName).
Match("id", "isolated").
Return("result", "isolated_result")
client := NewMyServiceClient(srv.Conn())
// ACT
resp, err := client.MyMethod(t.Context(), &MyRequest{Id: "isolated"})
// ASSERT
require.NoError(t, err)
require.Equal(t, "isolated_result", resp.Result)
}Health Timeout Configuration
func TestMyService_HealthTimeout(t *testing.T) {
// ARRANGE
// Configure the timeout for waiting for the remote server to become healthy
srv := sdk.NewServer(t,
sdk.WithRemote("localhost:4770", "http://localhost:4771"),
sdk.WithFileDescriptor(service.File_service_proto),
sdk.WithHealthCheckTimeout(15 * time.Second), // Wait up to 15 seconds
)
srv.ExpectUnary(MyService_MyMethod_FullMethodName).
Match("id", "timeout-test").
Return("result", "success")
client := NewMyServiceClient(srv.Conn())
// ACT
resp, err := client.MyMethod(t.Context(), &MyRequest{Id: "timeout-test"})
// ASSERT
require.NoError(t, err)
require.Equal(t, "success", resp.Result)
}Remote Mode with Error Handling
func TestMyService_RemoteWithError(t *testing.T) {
// ARRANGE
srv := sdk.NewServer(t,
sdk.WithRemote("localhost:4770", "http://localhost:4771"),
sdk.WithFileDescriptor(service.File_service_proto),
)
srv.ExpectUnary(MyService_MyMethod_FullMethodName).
Match("id", "error-case").
ReturnError(codes.Internal, "Remote service error")
client := NewMyServiceClient(srv.Conn())
// ACT
_, err := client.MyMethod(t.Context(), &MyRequest{Id: "error-case"})
// ASSERT
require.Error(t, err)
require.Equal(t, codes.Internal, status.Code(err))
require.Contains(t, err.Error(), "Remote service error")
}Parallel Tests with Remote Sessions
func TestMyService_ParallelExecution(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()),
)
srv.ExpectUnary(MyService_MyMethod_FullMethodName).
Match("id", "parallel-test").
Return("result", "parallel-success")
client := NewMyServiceClient(srv.Conn())
// ACT
resp, err := client.MyMethod(t.Context(), &MyRequest{Id: "parallel-test"})
// ASSERT
require.NoError(t, err)
require.Equal(t, "parallel-success", resp.Result)
}Remote Mode with Verification
func TestMyService_RemoteVerification(t *testing.T) {
// ARRANGE
srv := sdk.NewServer(t,
sdk.WithRemote("localhost:4770", "http://localhost:4771"),
sdk.WithFileDescriptor(service.File_service_proto),
sdk.WithSession(t.Name()),
)
srv.ExpectUnary(MyService_MyMethod_FullMethodName).
Match("id", "verify-test").
Times(2). // Expect exactly 2 calls
Return("result", "verified")
client := NewMyServiceClient(srv.Conn())
// ACT
_, _ = client.MyMethod(t.Context(), &MyRequest{Id: "verify-test"})
_, _ = client.MyMethod(t.Context(), &MyRequest{Id: "verify-test"})
// ASSERT
// Verification happens automatically due to Times(2) and passing t to NewServer
require.Equal(t, 2, srv.Called(MyService_MyMethod_FullMethodName))
}Custom HTTP Client
Use sdk.WithHTTPClient(...) when you need custom transport, tracing, or timeouts for REST management calls:
func TestMyService_RemoteWithCustomHTTPClient(t *testing.T) {
// ARRANGE
httpClient := &http.Client{Timeout: 3 * time.Second}
srv := sdk.NewServer(t,
sdk.WithRemote("localhost:4770", "http://localhost:4771"),
sdk.WithHTTPClient(httpClient),
sdk.WithFileDescriptor(service.File_service_proto),
sdk.WithSession(t.Name()),
)
srv.ExpectUnary(MyService_MyMethod_FullMethodName).
Match("id", "custom-http").
Return("result", "ok")
client := NewMyServiceClient(srv.Conn())
// ACT
resp, err := client.MyMethod(t.Context(), &MyRequest{Id: "custom-http"})
// ASSERT
require.NoError(t, err)
require.Equal(t, "ok", resp.Result)
}Context Propagation for Management Calls
Remote mode uses HTTP management APIs (/api/stubs, /api/history, /api/verify, /api/descriptors).
- Verification methods that take
tuset.Context(). - History/verification can also be called with explicit context helpers:
sdk.HistoryAllContext(...)sdk.HistoryCountContext(...)sdk.HistoryFilterByMethodContext(...)sdk.VerifyStubTimesErrContext(...)
Use ExpectationsWereMetContext(ctx) for context-aware verification:
func TestMyService_RemoteContextCancel(t *testing.T) {
srv := sdk.NewServer(t,
sdk.WithRemote("localhost:4770", "http://localhost:4771"),
sdk.WithSession(t.Name()),
)
// ... Arrange/Act ...
ctx, cancel := context.WithCancel(t.Context())
cancel()
err := srv.ExpectationsWereMetContext(ctx)
require.Error(t, err)
require.ErrorIs(t, err, context.Canceled)
}gRPC Timeout for Remote Calls
Use sdk.WithGRPCTimeout(...) to apply a default timeout to remote gRPC calls when request context has no deadline:
func TestMyService_RemoteWithGRPCTimeout(t *testing.T) {
// ARRANGE
srv := sdk.NewServer(t,
sdk.WithRemote("localhost:4770", "http://localhost:4771"),
sdk.WithSession(t.Name()),
sdk.WithGRPCTimeout(250*time.Millisecond),
sdk.WithFileDescriptor(service.File_service_proto),
)
srv.ExpectUnary(MyService_SlowMethod_FullMethodName).
Return(sdk.Delay(2*time.Second, "result", "ok"))
client := NewMyServiceClient(srv.Conn())
// ACT
_, err := client.SlowMethod(t.Context(), &MyRequest{})
// ASSERT
require.Error(t, err)
require.Equal(t, codes.DeadlineExceeded, status.Code(err))
}Differences from embedded mode
These are deliberate, not bugs. A test that relies on them behaves differently per mode.
| Behaviour | Embedded | Remote |
|---|---|---|
| Descriptors | frozen at NewServer | left registered on Close; the registry is keyed by file path, so the same files replace |
Run(fn) handlers | supported | panics — handlers are in-process and cannot be serialised |
| Bidi | Run or a static Match + SendStream stub | static stub only |
Reset() means the same in both modes: it drops the stubs the SDK registered and the calls it recorded. Remotely it sends DELETE /api/history, scoped to the session, so a parallel session keeps its own calls. Without WithSession there is nothing to scope by, and the purge clears the shared server's whole history — one more reason to give every remote test its own session.
Verification also differs from the server's own POST /api/verify: the SDK counts per stub ID, so several stubs sharing one method are checked independently, while /api/verify counts per service and method.
When to use it
Remote mode is worth its cost when one GripMock has to serve several test processes at once, when the state must outlive a single test binary, or when you want to watch the stubs in the web UI while the suite runs.
Against that: every call crosses the network, the process has to be running before the tests start, and tests that skip session isolation will overwrite each other's stubs. For a single Go test binary, embedded mode is the simpler choice.