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 runTestEx3() { 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 runTestEx3() // 2nd run of the test - will use playback runTestEx3() }
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 (Number4SimpleVCR) ¶
Example_simpleVCR is an example use of govcr. It shows a simple use of a Long Play cassette (i.e. compressed).
package main import ( "fmt" "io/ioutil" "strings" "github.com/seborama/govcr" ) const example4CassetteName = "MyCassette4" func runTestEx4() { // Create vcr and make http call vcr := govcr.NewVCR(example4CassetteName, 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 a simple use of a Long Play cassette (i.e. compressed). func main() { // Delete cassette to enable live HTTP call govcr.DeleteCassette(example4CassetteName, "") // 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 {TracksLoaded:0 TracksRecorded:1 TracksPlayed:0} 404 text/html; charset=UTF-8 true {TracksLoaded:1 TracksRecorded:0 TracksPlayed:1}
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 ¶ added in v1.6.0
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 Request ¶
A Request provides the request parameters. Notice of warning: 'Request' contains fields that are subject to shallow copy:
- url.URL which itself contains a pointer.
- Header which is a map.
- Body which is a slice.
As a result, when copying a 'Request', the shallow copy shares those above mentioned fields' data! A change to the (shallow) copy will also change the source object!
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 RequestExcludeHeaderFunc ¶
func RequestExcludeHeaderFunc(fn func(key string) bool) RequestFilter
RequestExcludeHeaderFunc is a hook function that is used to filter the Header.
Typically this can be used to remove / amend undesirable custom headers from the request.
For instance, if your application sends requests with a timestamp held in a custom header, you likely want to exclude it from the comparison to ensure that the request headers are considered a match with those saved on the cassette's track.
Parameters:
- parameter 1 - Name of header key in the Request
Return value: true - exclude header key from comparison false - retain header key for comparison
Deprecated - This function will be removed on or after April 25th 2019
func (RequestFilter) OnMethod ¶
func (r RequestFilter) OnMethod(method ...string) RequestFilter
OnMethod will return a request filter that will only apply 'r' if the request method matches one of the specified methods in the argument list. 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 request method matches one of the specified methods in the argument list.. 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 one of the supplied statuses. 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 int32 // TracksRecorded is the number of new tracks recorded by VCR. TracksRecorded int32 // 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 int32 }
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 // LongPlay will compress data on cassettes. LongPlay bool DisableRecording bool Logging bool CassettePath string // RemoveTLS will remove TLS from the Response when recording. // TLS information is rarely needed and takes up a lot of space. RemoveTLS bool }
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.