MCPcopy Index your code
hub / github.com/dnaeon/go-vcr

github.com/dnaeon/go-vcr @v4.0.7

Chat with this repo
repository ↗ · DeepWiki ↗ · release v4.0.7 ↗ · + Follow
131 symbols 581 edges 13 files 84 documented · 64% 6 cross-repo links updated 13d agov4.0.7 · 2026-06-25★ 1,3915 open issues
What it actually does AI analysis from the code graph — generated when you open this
loading…
README

go-vcr

Build Status Go Reference Go Report Card codecov

go-vcr simplifies testing by recording your HTTP interactions and replaying them in future runs in order to provide fast, deterministic and accurate testing of your code.

go-vcr was inspired by the VCR library for Ruby.

Installation

Install go-vcr by executing the command below:

$ go get -v gopkg.in/dnaeon/go-vcr.v4

Note, that if you are migrating from a previous version of go-vcr, you may need to re-create your tests cassettes.

Usage

A quick example of using go-vcr.

package helloworld_test

import (
    "path/filepath"
    "strings"
    "testing"

    "gopkg.in/dnaeon/go-vcr.v4/pkg/recorder"
)

func TestHelloWorld(t *testing.T) {
    // Create our recorder
    r, err := recorder.New(filepath.Join("testdata", strings.ReplaceAll(t.Name(), "/", "_")))
    if err != nil {
        t.Fatal(err)
    }
    t.Cleanup(func() {
        // Make sure recorder is stopped once done with it.
        if err := r.Stop(); err != nil {
            t.Error(err)
        }
    })

    client := r.GetDefaultClient()
    url := "https://go.dev/VERSION?m=text"

    resp, err := client.Get(url)
    if err != nil {
        t.Fatalf("Failed to get url %s: %s", url, err)
    }

    t.Logf("GET %s: %d\n", url, resp.StatusCode)
}

Running this test code for the first time will result in creating the testdata/TestHelloWorld.yaml cassette, which will contain the recorded HTTP interaction between our HTTP client and the remote server.

When we execute this test next time, what would happen is that go-vcr will replay the already recorded HTTP interactions from the cassette, instead of making actual external calls.

Please also check the examples directory from this repo for complete and ready to run examples.

You can also refer to the test cases for additional examples.

Custom YAML Marshaling Function

If you need control how YAML is encoded, set WithMarshalFunc with a custom func. This defaults to yaml.Marshal.

package marshalfunc_test

import (
    "bytes"
    "path/filepath"
    "strings"
    "testing"

    "go.yaml.in/yaml/v4"
    "gopkg.in/dnaeon/go-vcr.v4/pkg/recorder"
)

func TestCustomMarshalFunc(t *testing.T) {
    marshalFunc := func(in any) ([]byte, error) {
        var buf bytes.Buffer
        enc := yaml.NewEncoder(&buf)

        // Example of custom options from
        // https://pkg.go.dev/go.yaml.in/yaml/v4
        enc.CompactSeqIndent()
        enc.SetIndent(4)

        if err := enc.Encode(in); err != nil {
            return nil, err
        }
        return buf.Bytes(), nil
    }

    r, err := recorder.New(
        filepath.Join("testdata", strings.ReplaceAll(t.Name(), "/", "_")),
        recorder.WithMarshalFunc(marshalFunc),
    )
    if err != nil {
        t.Fatal(err)
    }
    t.Cleanup(func() {
        if err := r.Stop(); err != nil {
            t.Error(err)
        }
    })

    client := r.GetDefaultClient()
    resp, err := client.Get("https://go.dev/VERSION?m=text")
    if err != nil {
        t.Fatal(err)
    }
    resp.Body.Close()
}

Custom Request Matching

During replay mode, you can customize the way incoming requests are matched against the recorded request/response pairs by defining a recorder.MatcherFunc function.

For example, the following matcher will match on method, URL and body:

package matcher_test

import (
    "bytes"
    "io"
    "net/http"
    "path/filepath"
    "strings"
    "testing"

    "gopkg.in/dnaeon/go-vcr.v4/pkg/cassette"
    "gopkg.in/dnaeon/go-vcr.v4/pkg/recorder"
)

func customMatcher(r *http.Request, i cassette.Request) bool {
    if r.Body == nil || r.Body == http.NoBody {
        return cassette.DefaultMatcher(r, i)
    }

    reqBody, err := io.ReadAll(r.Body)
    if err != nil {
        return false
    }
    r.Body.Close()
    r.Body = io.NopCloser(bytes.NewBuffer(reqBody))

    return r.Method == i.Method && r.URL.String() == i.URL && string(reqBody) == i.Body
}

func TestCustomMatcher(t *testing.T) {
    r, err := recorder.New(
        filepath.Join("testdata", strings.ReplaceAll(t.Name(), "/", "_")),
        recorder.WithMatcher(customMatcher),
    )
    if err != nil {
        t.Fatal(err)
    }
    t.Cleanup(func() {
        if err := r.Stop(); err != nil {
            t.Error(err)
        }
    })

    client := r.GetDefaultClient()
    resp, err := client.Get("https://go.dev/VERSION?m=text")
    if err != nil {
        t.Fatal(err)
    }
    resp.Body.Close()
}

Hooks

Hooks in go-vcr are regular functions which take an HTTP interaction and are invoked at different stages of the playback.

You can use hooks to modify a request/response before it is saved on disk, before it is returned to the client, or anything else that you might want to do with it, e.g. you might want to simply log each captured interaction.

You often provide sensitive data, such as API credentials, when making requests against a service.

By default, this data will be stored in the recorded data but you probably don't want this.

Removing or replacing data before it is stored can be done by adding one or more Hooks to your Recorder.

There are different kinds of hooks, which are invoked in different stages of the playback. The supported hook kinds are AfterCaptureHook, BeforeSaveHook, BeforeResponseReplayHook and OnRecorderStop.

Here is an example that removes the Authorization header from all requests right after capturing a new interaction.

package hooks_test

import (
    "path/filepath"
    "strings"
    "testing"

    "gopkg.in/dnaeon/go-vcr.v4/pkg/cassette"
    "gopkg.in/dnaeon/go-vcr.v4/pkg/recorder"
)

func TestAfterCaptureHook(t *testing.T) {
    // A hook which removes Authorization headers from all requests
    hook := func(i *cassette.Interaction) error {
        delete(i.Request.Headers, "Authorization")
        return nil
    }

    r, err := recorder.New(
        filepath.Join("testdata", strings.ReplaceAll(t.Name(), "/", "_")),
        recorder.WithHook(hook, recorder.AfterCaptureHook),
        recorder.WithMatcher(cassette.NewDefaultMatcher(cassette.WithIgnoreAuthorization())),
    )
    if err != nil {
        t.Fatal(err)
    }
    t.Cleanup(func() {
        if err := r.Stop(); err != nil {
            t.Error(err)
        }
    })

    client := r.GetDefaultClient()
    resp, err := client.Get("https://go.dev/VERSION?m=text")
    if err != nil {
        t.Fatal(err)
    }
    resp.Body.Close()
}

Hooks added using recorder.AfterCaptureHook are applied right after an interaction is captured and added to the in-memory cassette. This may not always be what you need. For example if you modify an interaction using this hook kind then subsequent test code will see the edited response.

For instance, if a response body contains an OAuth access token that is needed for subsequent requests, then redacting the access token using a AfterCaptureHook will result in authorization failures in subsequent test code.

In such cases you would want to modify the recorded interactions right before they are saved on disk. For that purpose you should be using a BeforeSaveHook, e.g.

package hooks_test

import (
    "path/filepath"
    "strings"
    "testing"

    "gopkg.in/dnaeon/go-vcr.v4/pkg/cassette"
    "gopkg.in/dnaeon/go-vcr.v4/pkg/recorder"
)

func TestBeforeSaveHook(t *testing.T) {
    // Your test code will continue to see the real access token and
    // it is redacted before the recorded interactions are saved on disk
    hook := func(i *cassette.Interaction) error {
        if strings.Contains(i.Request.URL, "/oauth/token") {
            i.Response.Body = `{"access_token": "[REDACTED]"}`
        }
        return nil
    }

    r, err := recorder.New(
        filepath.Join("testdata", strings.ReplaceAll(t.Name(), "/", "_")),
        recorder.WithHook(hook, recorder.BeforeSaveHook),
    )
    if err != nil {
        t.Fatal(err)
    }
    t.Cleanup(func() {
        if err := r.Stop(); err != nil {
            t.Error(err)
        }
    })

    client := r.GetDefaultClient()
    resp, err := client.Get("https://go.dev/VERSION?m=text")
    if err != nil {
        t.Fatal(err)
    }
    resp.Body.Close()
}

Passing Through Requests

Sometimes you want to allow specific requests to pass through to the remote server without recording anything.

Globally, you can use ModePassthrough for this, but if you want to disable the recorder for individual requests, you can add Passthrough handlers to the recorder.

Here's an example to pass through requests to a specific endpoint:

package passthrough_test

import (
    "net/http"
    "path/filepath"
    "strings"
    "testing"

    "gopkg.in/dnaeon/go-vcr.v4/pkg/recorder"
)

func TestPassthrough(t *testing.T) {
    passthrough := func(req *http.Request) bool {
        return req.URL.Path == "/login"
    }

    r, err := recorder.New(
        filepath.Join("testdata", strings.ReplaceAll(t.Name(), "/", "_")),
        recorder.WithPassthrough(passthrough),
    )
    if err != nil {
        t.Fatal(err)
    }
    t.Cleanup(func() {
        if err := r.Stop(); err != nil {
            t.Error(err)
        }
    })

    client := r.GetDefaultClient()
    resp, err := client.Get("https://go.dev/VERSION?m=text")
    if err != nil {
        t.Fatal(err)
    }
    resp.Body.Close()
}

Debug Tracing

When a test fails because the recorder did not match the request you expected, it is often useful to see exactly what the recorder is doing.

The recorder.WithDebugWriter option allows API clients to configure an io.Writer that is used to write debug events via an internal slog.Logger instance.

package debug_test

import (
    "os"
    "path/filepath"
    "strings"
    "testing"

    "gopkg.in/dnaeon/go-vcr.v4/pkg/recorder"
)

func TestDebugTrace(t *testing.T) {
    r, err := recorder.New(
        filepath.Join("testdata", strings.ReplaceAll(t.Name(), "/", "_")),
        recorder.WithDebugWriter(os.Stderr),
    )
    if err != nil {
        t.Fatal(err)
    }
    t.Cleanup(func() {
        if err := r.Stop(); err != nil {
            t.Error(err)
        }
    })

    client := r.GetDefaultClient()
    resp, err := client.Get("https://go.dev/VERSION?m=text")
    if err != nil {
        t.Fatal(err)
    }
    resp.Body.Close()
}

The debug writer may also be enabled without changing the code by setting the VCR_DEBUG environment variable to true.

An explicit recorder.WithDebugWriter option always takes precedence over the environment variable.

VCR_DEBUG=true go test ./...

Server Side

VCR testing can also be used for creating server-side tests. Use the recorder.HTTPMiddleware with an HTTP handler in order to create fixtures from incoming requests and the handler's responses. Then, these requests can be replayed and compared against the recorded responses to create a regression test.

Rather than mocking/recording external HTTP interactions, this will record and replay incoming interactions with your application's HTTP server.

See an example here.

License

go-vcr is Open Source and licensed under the BSD License

Extension points exported contracts — how you extend this code

FS (Interface)
FS defines a generic filesystem interface. It allows to redefine storage without depending on a specific filesystem impl [1 …
pkg/cassette/fs.go
HookFunc (FuncType)
HookFunc represents a function, which will be invoked in different stages of the playback. The hook functions allow for
pkg/recorder/recorder.go
ReplayAssertFunc (FuncType)
ReplayAssertFunc is used to assert the results of replaying a recorded request against a handler. It receives the curren
pkg/cassette/server_replay.go
PassthroughFunc (FuncType)
PassthroughFunc is a predicate which determines whether a specific HTTP request is to be forwarded to the original endpo
pkg/recorder/recorder.go
MatcherFunc (FuncType)
MatcherFunc is a predicate, which returns true when the actual request matches an interaction from the cassette.
pkg/cassette/cassette.go
Option (FuncType)
Option is a function which configures the [Recorder].
pkg/recorder/recorder.go
MarshalFunc (FuncType)
MarshalFunc is a function which marshals an object to a byte slice.
pkg/cassette/cassette.go
DefaultMatcherOption (FuncType)
DefaultMatcherOption is a function which configures the default matcher.
pkg/cassette/cassette.go

Core symbols most depended-on inside this repo

New
called by 32
pkg/recorder/recorder.go
Stop
called by 29
pkg/recorder/recorder.go
GetDefaultClient
called by 29
pkg/recorder/recorder.go
debug
called by 23
pkg/recorder/recorder.go
Mode
called by 20
pkg/recorder/recorder.go
IsRecording
called by 17
pkg/recorder/recorder.go
String
called by 12
pkg/recorder/recorder.go
isPrintable
called by 10
pkg/cassette/cassette.go

Shape

Function 68
Method 41
Struct 12
FuncType 7
TypeAlias 2
Interface 1

Languages

Go100%

Modules by API surface

pkg/recorder/recorder.go37 symbols
pkg/cassette/cassette.go34 symbols
pkg/recorder/recorder_test.go23 symbols
pkg/cassette/fs.go9 symbols
pkg/cassette/cassette_test.go9 symbols
pkg/recorder/middleware.go6 symbols
pkg/cassette/server_replay.go5 symbols
examples/middleware_test.go2 symbols
examples/json_test.go2 symbols
pkg/recorder/example_test.go1 symbols
examples/simple_test.go1 symbols
examples/https_test.go1 symbols

For agents

$ claude mcp add go-vcr \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact

Ask about this repo answers extend the page