Tags: #go #http
Here’s the latest approach I’ve used: https://github.com/fastly/cli/pull/1374/commits/98742dcfebe896e7b97bfbdb8f72906aede71594
First, the test itself:
// pkg/commands/domainv1/domain_test.go
package domainv1_test
import (
"bytes"
"fmt"
"io"
"net/http"
"testing"
v1 "github.com/fastly/go-fastly/v9/fastly/domains/v1"
root "github.com/fastly/cli/pkg/commands/domainv1"
"github.com/fastly/cli/pkg/testutil"
)
func TestDomainV1Create(t *testing.T) {
fqdn := "www.example.com"
sid := "123"
did := "domain-id"
scenarios := []testutil.CLIScenario{
{
Args: "",
WantError: "error parsing arguments: required flag --fqdn not provided",
},
{
Args: fmt.Sprintf("--fqdn %s --service-id %s", fqdn, sid),
Client: &http.Client{
Transport: &testutil.MockRoundTripper{
Response: &http.Response{
StatusCode: http.StatusOK,
Status: http.StatusText(http.StatusOK),
Body: io.NopCloser(bytes.NewReader(testutil.GenJSON(v1.Data{
DomainID: did,
FQDN: fqdn,
ServiceID: &sid,
}))),
},
},
},
WantOutput: fmt.Sprintf("SUCCESS: Created domain '%s' (domain-id: %s, service-id: %s)", fqdn, did, sid),
},
}
testutil.RunCLIScenarios(t, []string{root.CommandName, "create"}, scenarios)
}
Second, the supporting test util functions:
// pkg/testutil/client.go
package testutil
import (
"encoding/json"
"net/http"
)
// MockRoundTripper implements [http.RoundTripper] for mocking HTTP responses
type MockRoundTripper struct {
Response *http.Response
Err error
}
// RoundTrip executes a single HTTP transaction, returning a Response for the
// provided Request.
func (m *MockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
return m.Response, m.Err
}
// GenJSON returns JSON encoding of data, or empty object in case of an error.
func GenJSON(data any) []byte {
b, err := json.Marshal(data)
if err != nil {
return []byte("{}")
}
return b
}
Third, modified scenario codebase:
// pkg/testutil/scenarios.go
var acf global.APIClientFactory
if scenario.Client != nil {
acf = func(_, _ string, _ bool) (api.Interface, error) {
fc, err := fastly.NewClientForEndpoint("no-key", "api.example.com")
if err != nil {
return nil, fmt.Errorf("failed to mock fastly.Client: %w", err)
}
fc.HTTPClient = scenario.Client
return fc, nil
}
} else {
acf = mock.APIClient(scenario.API)
}
opts.APIClientFactory = acf
package main
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"os"
"time"
)
type MockRoundTripper struct{}
func (rt MockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
fmt.Printf("http.Request: %+v\n", req)
res := &http.Response{
StatusCode: 404,
Body: ioutil.NopCloser(bytes.NewBufferString("Not Found!?")),
Header: make(http.Header),
}
return res, nil
}
func main() {
httpclient := &http.Client{
Timeout: time.Second * 10,
Transport: MockRoundTripper{},
}
response, err := httpclient.Get("https://www.integralist.co.uk/")
if err != nil {
fmt.Println(err.Error())
os.Exit(1)
}
defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body)
if err != nil {
fmt.Println(err.Error())
os.Exit(1)
}
fmt.Println(string(body))
fmt.Println(response.Status)
}
// modified from https://gist.github.com/jarcoal/8940980
//
package main
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"os"
"time"
)
type Responder func(*http.Request) (*http.Response, error)
type MockRoundTripperMultiples struct {
host string
responders map[string]Responder
}
func (rt MockRoundTripperMultiples) RoundTrip(req *http.Request) (*http.Response, error) {
key := req.Method + " " + req.URL.String()
for k, r := range rt.responders {
if k != key {
continue
}
return r(req)
}
return http.DefaultTransport.RoundTrip(req)
}
func (rt *MockRoundTripperMultiples) RegisterResponder(method, path string, responder Responder) {
if rt.responders == nil {
rt.responders = make(map[string]Responder)
}
rt.responders[method+" "+rt.BuildURL(path)] = responder
}
func (rt *MockRoundTripperMultiples) BuildURL(path string) string {
return "https://" + rt.host + path
}
func requestOK(*http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: 200,
Body: ioutil.NopCloser(bytes.NewBufferString("OK!!!")),
Header: make(http.Header),
}, nil
}
func requestBad(*http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: 400,
Body: ioutil.NopCloser(bytes.NewBufferString("Bad Request!??")),
Header: make(http.Header),
}, nil
}
func get(url string, httpclient *http.Client) *http.Response {
res, err := httpclient.Get(url)
if err != nil {
fmt.Println(err.Error())
os.Exit(1)
}
return res
}
func read(url string, httpclient *http.Client) {
res := get(url, httpclient)
body, err := ioutil.ReadAll(res.Body)
if err != nil {
fmt.Println(err.Error())
os.Exit(1)
}
defer res.Body.Close()
fmt.Println(string(body))
fmt.Println(res.Status)
}
func main() {
transport := &MockRoundTripperMultiples{
host: "www.integralist.co.uk",
}
transport.RegisterResponder("GET", "/good", requestOK)
transport.RegisterResponder("GET", "/bad", requestBad)
httpclient := &http.Client{
Timeout: time.Second * 10,
Transport: transport,
}
read("https://www.integralist.co.uk/good", httpclient)
read("https://www.integralist.co.uk/bad", httpclient)
// no registered 'responder' matches the following URL so it will fallback to
// using http.DefaultTransport.RoundTrip implementation to make a 'real'
// network request.
read("https://www.integralist.co.uk/about", httpclient)
}