Documentation
¶
Overview ¶
Package govcr records and replays HTTP interactions for offline unit / behavioural / integration tests thereby acting as an HTTP mock.
This project was inspired by php-vcr which is a PHP port of VCR for ruby.
For usage and more information, please refer to the project's README at:
https://github.com/seborama/govcr
Example (Number1SimpleVCR) ¶
Example_simpleVCR is an example use of govcr. It shows how to use govcr in the simplest case when the default http.Client suffices.
package main
import (
"fmt"
"io/ioutil"
"strings"
"github.com/seborama/govcr"
)
const example1CassetteName = "MyCassette1"
func runTestEx1() {
// Create vcr and make http call
vcr := govcr.NewVCR(example1CassetteName, nil)
resp, _ := vcr.Client.Get("http://www.example.com/foo")
// Show results
fmt.Printf("%d ", resp.StatusCode)
fmt.Printf("%s ", resp.Header.Get("Content-Type"))
body, _ := ioutil.ReadAll(resp.Body)
resp.Body.Close()
fmt.Printf("%v ", strings.Contains(string(body), "domain in examples without prior coordination or asking for permission."))
fmt.Printf("%+v\n", vcr.Stats())
}
// Example_simpleVCR is an example use of govcr.
// It shows how to use govcr in the simplest case when the default
// http.Client suffices.
func main() {
// Delete cassette to enable live HTTP call
govcr.DeleteCassette(example1CassetteName, "")
// 1st run of the test - will use live HTTP calls
runTestEx1()
// 2nd run of the test - will use playback
runTestEx1()
}
Output: 404 text/html; charset=UTF-8 true {TracksLoaded:0 TracksRecorded:1 TracksPlayed:0} 404 text/html; charset=UTF-8 true {TracksLoaded:1 TracksRecorded:0 TracksPlayed:1}
Example (Number2CustomClientVCR1) ¶
Example2 is an example use of govcr. It shows the use of a VCR with a custom Client. Here, the app executes a GET request.
package main
import (
"crypto/tls"
"fmt"
"io/ioutil"
"net/http"
"strings"
"time"
"github.com/seborama/govcr"
)
const example2CassetteName = "MyCassette2"
// myApp is an application container.
type myApp struct {
httpClient *http.Client
}
func (app *myApp) Get(url string) (*http.Response, error) {
return app.httpClient.Get(url)
}
func (app *myApp) Post(url string) (*http.Response, error) {
// beware: don't use a ReadCloser, only a Reader!
body := strings.NewReader(`{"Msg": "This is an example request"}`)
return app.httpClient.Post(url, "application/json", body)
}
func runTestEx2(app *myApp) {
var samples = []struct {
f func(string) (*http.Response, error)
body string
}{
{app.Get, "domain in examples without prior coordination or asking for permission."},
{app.Post, "404 - Not Found"},
}
// Instantiate VCR.
vcr := govcr.NewVCR(example2CassetteName,
&govcr.VCRConfig{
Client: app.httpClient,
})
// Inject VCR's http.Client wrapper.
// The original transport has been preserved, only just wrapped into VCR's.
app.httpClient = vcr.Client
for _, td := range samples {
// Run HTTP call
resp, _ := td.f("https://www.example.com/foo")
// Show results
fmt.Printf("%d ", resp.StatusCode)
fmt.Printf("%s ", resp.Header.Get("Content-Type"))
body, _ := ioutil.ReadAll(resp.Body)
resp.Body.Close()
fmt.Printf("%v - ", strings.Contains(string(body), td.body))
}
fmt.Printf("%+v\n", vcr.Stats())
}
// Example2 is an example use of govcr.
// It shows the use of a VCR with a custom Client.
// Here, the app executes a GET request.
func main() {
// Create a custom http.Transport.
tr := http.DefaultTransport.(*http.Transport)
tr.TLSClientConfig = &tls.Config{
InsecureSkipVerify: true, // just an example, not recommended
}
// Create an instance of myApp.
// It uses the custom Transport created above and a custom Timeout.
app := &myApp{
httpClient: &http.Client{
Transport: tr,
Timeout: 15 * time.Second,
},
}
// Delete cassette to enable live HTTP call
govcr.DeleteCassette(example2CassetteName, "")
// 1st run of the test - will use live HTTP calls
runTestEx2(app)
// 2nd run of the test - will use playback
runTestEx2(app)
}
Output: 404 text/html; charset=UTF-8 true - 404 text/html; charset=UTF-8 true - {TracksLoaded:0 TracksRecorded:2 TracksPlayed:0} 404 text/html; charset=UTF-8 true - 404 text/html; charset=UTF-8 true - {TracksLoaded:2 TracksRecorded:0 TracksPlayed:2}
Example (Number3HeaderExclusionVCR) ¶
Example_simpleVCR is an example use of govcr. It shows how to use govcr in the simplest case when the default http.Client suffices.
package main
import (
"fmt"
"io/ioutil"
"net/http"
"strings"
"time"
"github.com/seborama/govcr"
)
const example3CassetteName = "MyCassette3"
func runTestEx4() {
var samples = []struct {
method string
body string
}{
{"GET", "domain in examples without prior coordination or asking for permission."},
{"POST", "404 - Not Found"},
{"PUT", ""},
{"DELETE", ""},
}
// Create vcr
vcr := govcr.NewVCR(example3CassetteName,
&govcr.VCRConfig{
RequestFilters: govcr.RequestFilters{
govcr.RequestDeleteHeaderKeys("X-Custom-My-Date"),
},
})
for _, td := range samples {
// Create a request with our custom header
req, _ := http.NewRequest(td.method, "http://www.example.com/foo", nil)
req.Header.Add("X-Custom-My-Date", time.Now().String())
// Make http call
resp, _ := vcr.Client.Do(req)
// Show results
fmt.Printf("%d ", resp.StatusCode)
fmt.Printf("%s ", resp.Header.Get("Content-Type"))
body, _ := ioutil.ReadAll(resp.Body)
resp.Body.Close()
fmt.Printf("%v ", strings.Contains(string(body), td.body))
}
fmt.Printf("%+v\n", vcr.Stats())
}
// Example_simpleVCR is an example use of govcr.
// It shows how to use govcr in the simplest case when the default
// http.Client suffices.
func main() {
// Delete cassette to enable live HTTP call
govcr.DeleteCassette(example3CassetteName, "")
// 1st run of the test - will use live HTTP calls
runTestEx4()
// 2nd run of the test - will use playback
runTestEx4()
}
Output: 404 text/html; charset=UTF-8 true 404 text/html; charset=UTF-8 true 404 text/html; charset=UTF-8 true 404 text/html; charset=UTF-8 true {TracksLoaded:0 TracksRecorded:4 TracksPlayed:0} 404 text/html; charset=UTF-8 true 404 text/html; charset=UTF-8 true 404 text/html; charset=UTF-8 true 404 text/html; charset=UTF-8 true {TracksLoaded:4 TracksRecorded:0 TracksPlayed:4}
Example (Number6ConditionalRewrites) ¶
Example_simpleVCR is an example use of govcr. It shows how to use govcr in the simplest case when the default http.Client suffices.
package main
import (
"fmt"
"math/rand"
"net/http"
"time"
"github.com/seborama/govcr"
)
const example6CassetteName = "MyCassette6"
// Example6 is an example use of govcr.
// This will show how to do conditional rewrites.
// For example, your request has a "/order/{random}" path
// and we want to rewrite it to /order/1234 so we can match it later.
// We change the response status code.
// We add headers based on request method.
func runTestEx6(rng *rand.Rand) {
cfg := govcr.VCRConfig{
Logging: true,
}
// The filter will neutralize a value in the URL.
// In this case we rewrite /order/{random} to /order/1234
replacePath := govcr.RequestFilter(func(req govcr.Request) govcr.Request {
// Replace path with a predictable one.
req.URL.Path = "/order/1234"
return req
})
// Only execute when we match path.
replacePath = replacePath.OnPath(`example\.com\/order\/`)
// Add to request filters.
cfg.RequestFilters.Add(replacePath)
cfg.RequestFilters.Add(govcr.RequestDeleteHeaderKeys("X-Transaction-Id"))
// Add filters
cfg.ResponseFilters.Add(
// Always transfer 'X-Transaction-Id' as in example 5.
govcr.ResponseTransferHeaderKeys("X-Transaction-Id"),
// Change status 404 to 202.
func(resp govcr.Response) govcr.Response {
if resp.StatusCode == http.StatusNotFound {
resp.StatusCode = http.StatusAccepted
}
return resp
},
// Add header if method was "GET"
govcr.ResponseFilter(func(resp govcr.Response) govcr.Response {
resp.Header.Add("method-was-get", "true")
return resp
}).OnMethod(http.MethodGet),
// Add header if method was "POST"
govcr.ResponseFilter(func(resp govcr.Response) govcr.Response {
resp.Header.Add("method-was-post", "true")
return resp
}).OnMethod(http.MethodPost),
// Add actual request URL to header.
govcr.ResponseFilter(func(resp govcr.Response) govcr.Response {
url := resp.Request().URL
resp.Header.Add("get-url", url.String())
return resp
}).OnMethod(http.MethodGet),
)
orderID := fmt.Sprint(rng.Uint64())
vcr := govcr.NewVCR(example6CassetteName, &cfg)
// create a request with our custom header and a random url part.
req, err := http.NewRequest("POST", "http://www.example.com/order/"+orderID, nil)
if err != nil {
fmt.Println(err)
}
runExample6Request(req, vcr)
// create a request with our custom header and a random url part.
req, err = http.NewRequest("GET", "http://www.example.com/order/"+orderID, nil)
if err != nil {
fmt.Println(err)
}
runExample6Request(req, vcr)
}
func runExample6Request(req *http.Request, vcr *govcr.VCRControlPanel) {
req.Header.Add("X-Transaction-Id", time.Now().String())
// run the request
resp, err := vcr.Client.Do(req)
if err != nil {
fmt.Println(err)
return
}
// verify outcome
if req.Header.Get("X-Transaction-Id") != resp.Header.Get("X-Transaction-Id") {
fmt.Println("Header transaction Id verification failed - this would be the live request!")
} else {
fmt.Println("Header transaction Id verification passed - this would be the replayed track!")
}
// print outcome.
fmt.Println("Status code:", resp.StatusCode, " (should be 404 on real and 202 on replay)")
fmt.Println("method-was-get:", resp.Header.Get("method-was-get"), "(should never be true on GET)")
fmt.Println("method-was-post:", resp.Header.Get("method-was-post"), "(should be true on replay on POST)")
fmt.Println("get-url:", resp.Header.Get("get-url"), "(actual url of the request, not of the track)")
fmt.Printf("%+v\n", vcr.Stats())
}
// Example_simpleVCR is an example use of govcr.
// It shows how to use govcr in the simplest case when the default
// http.Client suffices.
func main() {
// Delete cassette to enable live HTTP call
govcr.DeleteCassette(example6CassetteName, "")
// We need a predictable RNG
rng := rand.New(rand.NewSource(6))
// 1st run of the test - will use live HTTP calls
runTestEx6(rng)
// 2nd run of the test - will use playback
runTestEx6(rng)
}
Output: Header transaction Id verification failed - this would be the live request! Status code: 404 (should be 404 on real and 202 on replay) method-was-get: (should never be true on GET) method-was-post: (should be true on replay on POST) get-url: (actual url of the request, not of the track) {TracksLoaded:0 TracksRecorded:1 TracksPlayed:0} Header transaction Id verification failed - this would be the live request! Status code: 404 (should be 404 on real and 202 on replay) method-was-get: (should never be true on GET) method-was-post: (should be true on replay on POST) get-url: (actual url of the request, not of the track) {TracksLoaded:0 TracksRecorded:2 TracksPlayed:0} Header transaction Id verification passed - this would be the replayed track! Status code: 202 (should be 404 on real and 202 on replay) method-was-get: (should never be true on GET) method-was-post: true (should be true on replay on POST) get-url: (actual url of the request, not of the track) {TracksLoaded:2 TracksRecorded:0 TracksPlayed:1} Header transaction Id verification passed - this would be the replayed track! Status code: 202 (should be 404 on real and 202 on replay) method-was-get: true (should never be true on GET) method-was-post: (should be true on replay on POST) get-url: http://www.example.com/order/7790075977082629872 (actual url of the request, not of the track) {TracksLoaded:2 TracksRecorded:0 TracksPlayed:2}
Example (Number7BodyInjection) ¶
Example_number7BodyInjection will show how bodies can be rewritten. We will take a varying ID from the request URL, neutralize it and also change the ID in the body of the response.
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"math/rand"
"net/http"
"net/http/httptest"
"regexp"
"github.com/seborama/govcr"
)
const example7CassetteName = "MyCassette7"
// runTestEx7 is an example use of govcr.
// This will show how bodies can be rewritten.
// We will take a varying ID from the request URL, neutralize it and also change the ID in the body of the response.
func runTestEx7(rng *rand.Rand) {
cfg := govcr.VCRConfig{
Logging: true,
}
// Order is out example body we want to modify.
type Order struct {
ID string `json:"id"`
Name string `json:"name"`
}
// Regex to extract the ID from the URL.
reOrderID := regexp.MustCompile(`/order/([^/]+)`)
// Create a local test server that serves out responses.
handler := func(w http.ResponseWriter, r *http.Request) {
id := reOrderID.FindStringSubmatch(r.URL.String())
if len(id) < 2 {
w.WriteHeader(404)
return
}
w.WriteHeader(200)
b, err := json.Marshal(Order{
ID: id[1],
Name: "Test Order",
})
if err != nil {
w.WriteHeader(500)
return
}
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(200)
w.Write(b)
}
server := httptest.NewServer(http.HandlerFunc(handler))
defer server.Close()
// The filter will neutralize a value in the URL.
// In this case we rewrite /order/{random} to /order/1234
// and replacing the host so it doesn't depend on the random port number.
replacePath := govcr.RequestFilter(func(req govcr.Request) govcr.Request {
req.URL.Path = "/order/1234"
req.URL.Host = "127.0.0.1"
return req
})
// Only execute when we match path.
cfg.RequestFilters.Add(replacePath.OnPath(`/order/`))
cfg.ResponseFilters.Add(
govcr.ResponseFilter(func(resp govcr.Response) govcr.Response {
req := resp.Request()
// Find the requested ID:
orderID := reOrderID.FindStringSubmatch(req.URL.String())
// Unmarshal body.
var o Order
err := json.Unmarshal(resp.Body, &o)
if err != nil {
panic(err)
}
// Change the ID
o.ID = orderID[1]
// Replace the body.
resp.Body, err = json.Marshal(o)
if err != nil {
panic(err)
}
return resp
}).OnStatus(200),
)
orderID := fmt.Sprint(rng.Uint64())
vcr := govcr.NewVCR(example7CassetteName, &cfg)
// create a request with our custom header and a random url part.
req, err := http.NewRequest("GET", server.URL+"/order/"+orderID, nil)
if err != nil {
fmt.Println(err)
}
// run the request
resp, err := vcr.Client.Do(req)
if err != nil {
fmt.Println("Error:", err)
return
}
// print outcome.
// Remove host name for consistent output
req.URL.Host = "127.0.0.1"
fmt.Println("GET", req.URL.String())
fmt.Println("Status code:", resp.StatusCode)
body, _ := ioutil.ReadAll(resp.Body)
fmt.Println("Returned Body:", string(body))
fmt.Printf("%+v\n", vcr.Stats())
}
// Example_number7BodyInjection will show how bodies can be rewritten.
// We will take a varying ID from the request URL, neutralize it and also change the ID in the body of the response.
func main() {
// Delete cassette to enable live HTTP call
govcr.DeleteCassette(example7CassetteName, "")
// We need a predictable RNG
rng := rand.New(rand.NewSource(7))
// 1st run of the test - will use live HTTP calls
runTestEx7(rng)
// 2nd run of the test - will use playback
runTestEx7(rng)
}
Output: GET http://127.0.0.1/order/8475284246537043955 Status code: 200 Returned Body: {"id":"8475284246537043955","name":"Test Order"} {TracksLoaded:0 TracksRecorded:1 TracksPlayed:0} GET http://127.0.0.1/order/2135276795452531224 Status code: 200 Returned Body: {"id":"2135276795452531224","name":"Test Order"} {TracksLoaded:1 TracksRecorded:0 TracksPlayed:1}
Index ¶
- func CassetteExistsAndValid(cassetteName, cassettePath string) bool
- func DeleteCassette(cassetteName, cassettePath string) error
- func GetFirstValue(hdr http.Header, key string) string
- type Request
- type RequestFilter
- type RequestFilters
- type Response
- type ResponseFilter
- type ResponseFilters
- type Stats
- type VCRConfig
- type VCRControlPanel
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func CassetteExistsAndValid ¶
CassetteExistsAndValid verifies a cassette file exists and is seemingly valid.
func DeleteCassette ¶
DeleteCassette removes the cassette file from disk.
func GetFirstValue ¶
GetFirstValue is a utility function that extracts the first value of a header key. The reason for this function is that some servers require case sensitive headers which prevent the use of http.Header.Get() as it expects header keys to be canonicalized.
Types ¶
type RequestFilter ¶
A RequestFilter can be used to remove / amend undesirable header / body elements from the request.
For instance, if your application sends requests with a timestamp held in a part of the header / body, you likely want to remove it or force a static timestamp via RequestFilterFunc to ensure that the request body matches those saved on the cassette's track.
A Filter should return the request with any modified values.
func RequestAddHeaderValue ¶
func RequestAddHeaderValue(key, value string) RequestFilter
RequestAddHeaderValue will add or overwrite a header to the request before the request is matched against the cassette.
func RequestDeleteHeaderKeys ¶
func RequestDeleteHeaderKeys(keys ...string) RequestFilter
RequestDeleteHeaderKeys will delete one or more header keys on the request before the request is matched against the cassette.
func (RequestFilter) OnMethod ¶
func (r RequestFilter) OnMethod(method string) RequestFilter
OnMethod will return a new filter that will only apply 'r' if the method of the request matches. Original filter is unmodified.
func (RequestFilter) OnPath ¶
func (r RequestFilter) OnPath(pathRegEx string) RequestFilter
OnPath will return a request filter that will only apply 'r' if the url string of the request matches the supplied regex. Original filter is unmodified.
type RequestFilters ¶
type RequestFilters []RequestFilter
RequestFilters is a slice of RequestFilter
func (*RequestFilters) Add ¶
func (r *RequestFilters) Add(filters ...RequestFilter)
Add one or more filters at the end of the filter chain.
func (*RequestFilters) Prepend ¶
func (r *RequestFilters) Prepend(filters ...RequestFilter)
Prepend one or more filters before the current ones.
type Response ¶
type Response struct {
// The content returned in the response.
Body []byte
Header http.Header
StatusCode int
// contains filtered or unexported fields
}
Response provides the response parameters. When returned from a ResponseFilter these values will be returned instead.
type ResponseFilter ¶
ResponseFilter is a hook function that is used to filter the Response Header / Body.
It works similarly to RequestFilterFunc but applies to the Response and also receives a copy of the Request context (if you need to pick info from it to override the response).
Return the modified response.
func ResponseAddHeaderValue ¶
func ResponseAddHeaderValue(key, value string) ResponseFilter
ResponseAddHeaderValue will add/overwrite a header to the response when it is returned from vcr playback.
func ResponseChangeBody ¶
func ResponseChangeBody(fn func(b []byte) []byte) ResponseFilter
ResponseChangeBody will allows to change the body. Supply a function that does input to output transformation.
func ResponseDeleteHeaderKeys ¶
func ResponseDeleteHeaderKeys(keys ...string) ResponseFilter
ResponseDeleteHeaderKeys will delete one or more headers on the response when returned from vcr playback.
func ResponseTransferHeaderKeys ¶
func ResponseTransferHeaderKeys(keys ...string) ResponseFilter
ResponseTransferHeaderKeys will transfer one or more header from the Request to the Response.
func (ResponseFilter) OnMethod ¶
func (r ResponseFilter) OnMethod(method string) ResponseFilter
OnMethod will return a Response filter that will only apply 'r' if the method of the response matches. Original filter is unmodified.
func (ResponseFilter) OnPath ¶
func (r ResponseFilter) OnPath(pathRegEx string) ResponseFilter
OnPath will return a Response filter that will only apply 'r' if the url string of the Response matches the supplied regex. Original filter is unmodified.
func (ResponseFilter) OnStatus ¶
func (r ResponseFilter) OnStatus(status int) ResponseFilter
OnStatus will return a Response filter that will only apply 'r' if the response status matches. Original filter is unmodified.
type ResponseFilters ¶
type ResponseFilters []ResponseFilter
ResponseFilters is a slice of ResponseFilter
func (*ResponseFilters) Add ¶
func (r *ResponseFilters) Add(filters ...ResponseFilter)
Add one or more filters at the end of the filter chain.
func (*ResponseFilters) Prepend ¶
func (r *ResponseFilters) Prepend(filters ...ResponseFilter)
Prepend one or more filters before the current ones.
type Stats ¶
type Stats struct {
// TracksLoaded is the number of tracks that were loaded from the cassette.
TracksLoaded int
// TracksRecorded is the number of new tracks recorded by VCR.
TracksRecorded int
// TracksPlayed is the number of tracks played back straight from the cassette.
// I.e. tracks that were already present on the cassette and were played back.
TracksPlayed int
}
Stats holds information about the cassette and VCR runtime.
type VCRConfig ¶
type VCRConfig struct {
Client *http.Client
// Filter to run before request is matched against cassettes.
RequestFilters RequestFilters
// Filter to run before a response is returned.
ResponseFilters ResponseFilters
DisableRecording bool
Logging bool
CassettePath string
}
VCRConfig holds a set of options for the VCR.
type VCRControlPanel ¶
VCRControlPanel holds the parts of a VCR that can be interacted with. Client is the HTTP client associated with the VCR.
func NewVCR ¶
func NewVCR(cassetteName string, vcrConfig *VCRConfig) *VCRControlPanel
NewVCR creates a new VCR and loads a cassette. A RoundTripper can be provided when a custom Transport is needed (for example to provide certificates, etc)
func (*VCRControlPanel) Stats ¶
func (vcr *VCRControlPanel) Stats() Stats
Stats returns Stats about the cassette and VCR session.