Google Cloud Client Libraries for Go providing documentation, authentication patterns, and utility packages for civil time types and HTTP/gRPC recording/replay functionality
The rpcreplay package supports the capture and replay of gRPC calls. Its main goal is to improve testing by allowing you to record gRPC interactions with a real service, then replay them in tests as an "automatic mock" that is fast and flake-free.
Status: This package is EXPERIMENTAL and subject to change without notice.
import "cloud.google.com/go/rpcreplay"The rpcreplay package allows you to:
This approach provides several benefits for testing:
The package works by providing gRPC dial options that intercept calls. During recording, calls are forwarded to the real service and logged. During replay, calls are matched against the log and recorded responses are returned.
To record gRPC calls:
Recorder with NewRecorder or NewRecorderWriterDialOptions() to grpc.DialTo replay recorded calls:
Replayer with NewReplayer or NewReplayerReaderDialOptions() to grpc.Dial, or use Connection() for a fake connectiontype Recorder struct {
BeforeFunc func(string, proto.Message) error
// Has unexported fields.
}A Recorder records gRPC RPCs for later playback.
Fields:
BeforeFunc: Optional callback function that can inspect and modify requests and responses before they are written to the replay file. It does not modify messages sent to the actual service. Called with the method name and the message. If it returns an error, that error is returned to the client. Only executed for unary RPCs; streaming RPCs are not supported.func NewRecorder(filename string, initial []byte) (*Recorder, error)Creates a recorder that writes to filename. The file will also store initial state for retrieval during replay. You must call Close() on the Recorder to ensure all data is written.
Parameters:
filename: Path to the file where interactions will be recordedinitial: Optional initial state (e.g., random seed, timestamp) to be saved for replayReturns: A new Recorder instance or an error if the file cannot be created.
Example:
import (
"context"
"log"
"time"
"cloud.google.com/go/rpcreplay"
"google.golang.org/api/option"
"google.golang.org/grpc"
)
func recordGRPCCalls() {
// Serialize initial state
timeNow := time.Now()
initialBytes, _ := timeNow.MarshalBinary()
// Create recorder
rec, err := rpcreplay.NewRecorder("recordings/test.rpclog", initialBytes)
if err != nil {
log.Fatal(err)
}
defer rec.Close()
// Optional: Set up callback
rec.BeforeFunc = func(method string, msg proto.Message) error {
log.Printf("Recording: %s", method)
return nil
}
// Create gRPC connection with recording enabled
conn, err := grpc.Dial("api.example.com:443",
rec.DialOptions()...,
grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(nil, "")))
if err != nil {
log.Fatal(err)
}
defer conn.Close()
// Use connection to make gRPC calls
// These calls will be recorded
}func NewRecorderWriter(w io.Writer, initial []byte) (*Recorder, error)Creates a recorder that writes to an arbitrary io.Writer. The initial bytes will also be written to w for retrieval during replay. You must call Close() on the Recorder to ensure all data is written.
Parameters:
w: Writer where interactions will be recordedinitial: Optional initial state to be saved for replayReturns: A new Recorder instance or an error.
Example:
var buf bytes.Buffer
rec, err := rpcreplay.NewRecorderWriter(&buf, []byte("initial-state"))
if err != nil {
log.Fatal(err)
}
defer rec.Close()func (r *Recorder) DialOptions() []grpc.DialOptionReturns the options that must be passed to grpc.Dial to enable recording. These options install interceptors that capture unary and streaming RPCs.
Returns: Slice of gRPC dial options for recording.
Example:
rec, _ := rpcreplay.NewRecorder("test.rpclog", nil)
defer rec.Close()
// Combine recorder options with other dial options
conn, err := grpc.Dial(serverAddress,
append(rec.DialOptions(),
grpc.WithTransportCredentials(creds),
grpc.WithBlock())...)func (r *Recorder) Close() errorSaves any unwritten information to the recording file or writer. You must call this method to ensure all recorded data is persisted.
Returns: An error if the close operation fails.
Example:
rec, _ := rpcreplay.NewRecorder("test.rpclog", nil)
defer func() {
if err := rec.Close(); err != nil {
log.Printf("Failed to close recorder: %v", err)
}
}()type Replayer struct {
BeforeFunc func(string, proto.Message) error
// Has unexported fields.
}A Replayer replays a set of RPCs saved by a Recorder.
Fields:
BeforeFunc: Optional callback function that can inspect and modify requests before they are matched against responses from the replay file. Called with the method name and the message. If it returns an error, that error is returned to the client. Only executed for unary RPCs; streaming RPCs are not supported.func NewReplayer(filename string) (*Replayer, error)Creates a Replayer that reads from filename.
Parameters:
filename: Path to the file containing recorded interactionsReturns: A new Replayer instance or an error if the file cannot be read.
Example:
func replayGRPCCalls() {
rep, err := rpcreplay.NewReplayer("recordings/test.rpclog")
if err != nil {
log.Fatal(err)
}
defer rep.Close()
// Retrieve initial state
var timeNow time.Time
if err := timeNow.UnmarshalBinary(rep.Initial()); err != nil {
log.Fatal(err)
}
// Get connection for replay
conn, err := rep.Connection()
if err != nil {
log.Fatal(err)
}
defer conn.Close()
// Use connection to make the same gRPC calls
// These will return recorded responses
}func NewReplayerReader(r io.Reader) (*Replayer, error)Creates a Replayer that reads from an io.Reader.
Parameters:
r: Reader containing recorded interactionsReturns: A new Replayer instance or an error.
Example:
data, _ := os.ReadFile("test.rpclog")
rep, err := rpcreplay.NewReplayerReader(bytes.NewReader(data))func (rep *Replayer) Connection() (*grpc.ClientConn, error)Returns a fake gRPC connection suitable for replaying. This is more convenient than calling grpc.Dial with DialOptions() since a real connection is not necessary for replay.
Returns: A gRPC client connection configured for replay, or an error.
Example:
rep, _ := rpcreplay.NewReplayer("test.rpclog")
defer rep.Close()
conn, err := rep.Connection()
if err != nil {
log.Fatal(err)
}
defer conn.Close()
// Use conn to create service clients
client := pb.NewMyServiceClient(conn)func (rep *Replayer) DialOptions() []grpc.DialOptionReturns the options that must be passed to grpc.Dial to enable replaying. Use this if you need to combine replay with real dialing for some reason; otherwise, prefer Connection().
Returns: Slice of gRPC dial options for replaying.
Example:
rep, _ := rpcreplay.NewReplayer("test.rpclog")
defer rep.Close()
conn, err := grpc.Dial(serverAddress, rep.DialOptions()...)func (rep *Replayer) Initial() []byteReturns the initial state saved by the Recorder. Use this to restore random seeds, timestamps, or other values that were used during recording.
Returns: The initial state bytes.
Example:
rep, _ := rpcreplay.NewReplayer("test.rpclog")
// Restore timestamp from initial state
var recordedTime time.Time
if err := recordedTime.UnmarshalBinary(rep.Initial()); err != nil {
log.Fatal(err)
}func (rep *Replayer) SetLogFunc(f func(format string, v ...interface{}))Sets a function to be used for debug logging. The function should be safe to call from multiple goroutines. Use this to debug replay issues.
Parameters:
f: Logging function with Printf-style signatureExample:
rep, _ := rpcreplay.NewReplayer("test.rpclog")
// Enable debug logging
rep.SetLogFunc(func(format string, v ...interface{}) {
log.Printf("[REPLAY] "+format, v...)
})func (rep *Replayer) Close() errorCloses the Replayer and releases associated resources.
Returns: An error if the close operation fails.
Example:
rep, _ := rpcreplay.NewReplayer("test.rpclog")
defer rep.Close()func Fprint(w io.Writer, filename string) errorReads the entries from filename and writes them to w in human-readable form. Intended for debugging replay files.
Parameters:
w: Writer where human-readable output will be writtenfilename: Path to the replay fileReturns: An error if reading or writing fails.
Example:
// Print replay file contents to stdout
err := rpcreplay.Fprint(os.Stdout, "test.rpclog")
if err != nil {
log.Fatal(err)
}func FprintReader(w io.Writer, r io.Reader) errorReads the entries from r and writes them to w in human-readable form. Intended for debugging replay data.
Parameters:
w: Writer where human-readable output will be writtenr: Reader containing replay dataReturns: An error if reading or writing fails.
Example:
data, _ := os.ReadFile("test.rpclog")
err := rpcreplay.FprintReader(os.Stdout, bytes.NewReader(data))Tests often use random or time-sensitive values to create unique resources or for other purposes. These values must be consistent between recording and replay. The initial state mechanism allows you to save and restore such values.
// Create some initial state
timeNow := time.Now()
randomSeed := rand.Int63()
// Serialize it
var buf bytes.Buffer
binary.Write(&buf, binary.LittleEndian, timeNow.Unix())
binary.Write(&buf, binary.LittleEndian, randomSeed)
// Pass to recorder
rec, _ := rpcreplay.NewRecorder("test.rpclog", buf.Bytes())// Get initial state
rep, _ := rpcreplay.NewReplayer("test.rpclog")
initialBytes := rep.Initial()
// Deserialize it
buf := bytes.NewReader(initialBytes)
var timeUnix int64
var randomSeed int64
binary.Read(buf, binary.LittleEndian, &timeUnix)
binary.Read(buf, binary.LittleEndian, &randomSeed)
timeNow := time.Unix(timeUnix, 0)
rand.Seed(randomSeed)Both Recorders and Replayers support BeforeFunc callbacks for inspecting and modifying messages:
Common uses include:
Important: If you modify requests in the recorder's BeforeFunc, you must apply the same modifications in the replayer's BeforeFunc, or matching will fail.
import "google.golang.org/protobuf/proto"
// Define a function to scrub messages
func scrubSensitive(method string, msg proto.Message) error {
// Type switch on message types
switch m := msg.(type) {
case *pb.CreateUserRequest:
// Clear sensitive field
m.Password = "REDACTED"
case *pb.AuthRequest:
m.ApiKey = "REDACTED"
}
return nil
}
// Apply to both recorder and replayer
rec.BeforeFunc = scrubSensitive
rep.BeforeFunc = scrubSensitiveThe replayer matches unary RPC requests by method name and request message contents. Nondeterministic call ordering is generally not a problem as long as:
Streaming RPCs are matched only by method name (no message content matching). Two streams with the same method name that are started concurrently may replay in the wrong order. This is a known limitation.
The replayer delivers responses immediately without waiting for other RPCs. This can violate causality in some scenarios. For example, in a Pub/Sub application where one goroutine publishes and another subscribes, the Subscribe call might return its recorded response before the Publish call begins.
CloseSend is not recordedpackage myservice_test
import (
"context"
"flag"
"testing"
"time"
"cloud.google.com/go/rpcreplay"
pb "example.com/myservice/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
var (
record = flag.Bool("record", false, "record gRPC interactions")
)
func TestGRPCService(t *testing.T) {
ctx := context.Background()
var conn *grpc.ClientConn
var err error
if *record {
// Recording mode
timeNow := time.Now()
initialBytes, _ := timeNow.MarshalBinary()
rec, err := rpcreplay.NewRecorder("testdata/service.rpclog", initialBytes)
if err != nil {
t.Fatal(err)
}
defer rec.Close()
// Optional: Set up callback for scrubbing
rec.BeforeFunc = func(method string, msg proto.Message) error {
t.Logf("Recording: %s", method)
// Scrub sensitive data here
return nil
}
// Dial with recording enabled
creds, _ := credentials.NewClientTLSFromFile("cert.pem", "")
conn, err = grpc.Dial("api.example.com:443",
append(rec.DialOptions(),
grpc.WithTransportCredentials(creds))...)
if err != nil {
t.Fatal(err)
}
defer conn.Close()
} else {
// Replay mode
rep, err := rpcreplay.NewReplayer("testdata/service.rpclog")
if err != nil {
t.Fatal(err)
}
defer rep.Close()
// Restore initial state
var timeNow time.Time
if err := timeNow.UnmarshalBinary(rep.Initial()); err != nil {
t.Fatal(err)
}
// Optional: Set up callback (must match recorder's transformations)
rep.BeforeFunc = func(method string, msg proto.Message) error {
t.Logf("Replaying: %s", method)
return nil
}
// Enable debug logging
rep.SetLogFunc(t.Logf)
// Get fake connection
conn, err = rep.Connection()
if err != nil {
t.Fatal(err)
}
defer conn.Close()
}
// Create service client
client := pb.NewMyServiceClient(conn)
// Make gRPC calls (recorded or replayed)
resp, err := client.GetResource(ctx, &pb.GetResourceRequest{
Id: "test-resource-123",
})
if err != nil {
t.Fatal(err)
}
// Verify response
if resp.Name == "" {
t.Error("Expected non-empty name")
}
}To use this test:
# Record interactions (requires credentials and network)
go test -record -v
# Replay interactions (no credentials or network needed)
go test -vWhen replay fails to match requests, use these techniques:
rep.SetLogFunc(func(format string, v ...interface{}) {
log.Printf("[REPLAY DEBUG] "+format, v...)
})// Print replay file in human-readable format
err := rpcreplay.Fprint(os.Stdout, "test.rpclog")Ensure that any modifications made in the recorder's BeforeFunc are also made in the replayer's BeforeFunc.
Make sure the requests during replay exactly match those during recording, including all fields.
Store recordings in version control: Commit replay files for consistent test behavior across environments
Use initial state for time-dependent tests: Save timestamps, random seeds, or other values that affect behavior
Scrub sensitive data: Use BeforeFunc to redact passwords, API keys, and other secrets before writing to replay files
Keep BeforeFunc consistent: Apply the same transformations during both recording and replay
Enable debug logging during development: Use SetLogFunc to troubleshoot matching issues
Inspect replay files: Use Fprint to examine recorded interactions in human-readable form
Re-record when APIs change: Update recordings when the gRPC service changes to avoid stale test data
Test both modes periodically: Run tests in recording mode occasionally to ensure your application still works with the real service
Be aware of streaming limitations: Avoid concurrent streams with the same method name
Handle causality carefully: Be aware that replay doesn't preserve timing relationships between RPCs
Install with Tessl CLI
npx tessl i tessl/golang-cloud-google-com--go@0.123.0