如何对将http调用包装到外部服务的处理程序进行单元测试

i have a handler function which is called CreateObject. This function wraps at the same time a POST request to an external API which i dont control. If I want to unit test it, the problem I face is that I can't be posting new objects to the external service every time I run the test. So I would like to know if there is a way to mock this with Go or any workaround.

Many thanks.

package main

func main() {

router := mux.NewRouter()

router.HandleFunc("/groups", services.CreateObject).Methods("POST")
c := cors.New(cors.Options{
    AllowedOrigins:   []string{"*"},
    AllowCredentials: true,
    AllowedMethods:   []string{"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"},
    AllowedHeaders:   []string{"*"},
    ExposedHeaders:   []string{"*"},
})

handler := c.Handler(router)

http.ListenAndServe(":3000", handler)

package objects

  func CreateObject(w http.ResponseWriter, r *http.Request) {



        var newobject Object
        _ = json.NewDecoder(r.Body).Decode(&newobject )

        //Do things

        jsonStr, err := json.Marshal(newobject)
        if err != nil {
            fmt.Println(err)
            return
        }

        req, err := http.NewRequest("POST", ExternalURL+"/object", bytes.NewBuffer(jsonStr))


        if err != nil {
            fmt.Println(err)
            return
        }

        client := &http.Client{}
        resp, err := client.Do(req)
        if err != nil {
            panic(err)
        }
        defer resp.Body.Close()

        body, _ := ioutil.ReadAll(resp.Body)


        if resp.StatusCode < 200 || resp.StatusCode > 299 {
            w.WriteHeader(resp.StatusCode)
            w.Header().Set("Content-Type", "application/json")
            w.Write(body)

        } else {
            w.WriteHeader(201)
        }



}

You have a couple options:

1) You can define an interface that has the exported method set of the http.Client. You can then make a package-level variable of this type, which defaults to a *http.Client. Instead of using a *http.Client in CreateObject, you'd use this variable. Since it's an interface, you can mock out the client easily. The interface would look like this:

type HTTPClient interface {
    Do(req *http.Request) (*http.Response, error)
    Get(url string) (resp *http.Response, err error)
    Post(url string, contentType string, body io.Reader) (resp *http.Response, err error)
    PostForm(url string, data url.Values) (resp *http.Response, err error)
    Head(url string) (resp *http.Response, err error)
}

Since you only call Do(), however, your mock only needs to define an actual test implementation for Do. We often use a function-field style for this:

type MockClient struct {
    DoFunc func(req *http.Request) (*http.Response, error)
    // other function fields, if you need them
}

func (m MockClient) Do(req *http.Request) (r *http.Response, err error) {
    if m.DoFunc != nil {
        r, err = m.DoFunc(req)
    }
    return
}

// Define the other 4 methods of the HTTPclient as trivial returns

var mockClient HTTPClient = MockClient{
    DoFunc: func(req *http.Request) (*http.Response, error) {
        return nil, nil
    },
}

var mockClientFail HTTPClient = MockClient{
    DoFunc: func(req *http.Request) (*http.Response, error) {
        return nil, fmt.Errorf("failed")
    },
}

2) Stand up your own HTTP mock server on a localhost port and, within your tests, change your ExternalURL variable to point to it instead. This allows you to actually test the dialing (which makes it more of a functional test than a unit test), while still effectively "mocking" the external endpoint.

In either case, make sure you also write some regression tests to make sure the external endpoint still works as expected.

Edit: Per dm03514, Go already has an mock HTTP server built in: https://golang.org/pkg/net/http/httptest/