如何对包装外部服务的处理程序进行单元测试?

huangapple go评论88阅读模式
英文:

How to unit test a handler which wraps an http call to an external service

问题

我有一个名为CreateObject的处理函数。该函数同时包装了对一个我无法控制的外部API的POST请求。如果我想对它进行单元测试,我面临的问题是每次运行测试时都不能向外部服务发送新的对象。所以我想知道是否有一种方法可以在Go中模拟这个或者有其他的解决方法。

非常感谢。

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)
		}


	
}
英文:

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(&quot;/groups&quot;, services.CreateObject).Methods(&quot;POST&quot;)
c := cors.New(cors.Options{
	AllowedOrigins:   []string{&quot;*&quot;},
	AllowCredentials: true,
	AllowedMethods:   []string{&quot;GET&quot;, &quot;POST&quot;, &quot;PUT&quot;, &quot;DELETE&quot;, &quot;PATCH&quot;, &quot;OPTIONS&quot;},
	AllowedHeaders:   []string{&quot;*&quot;},
	ExposedHeaders:   []string{&quot;*&quot;},
})

handler := c.Handler(router)

http.ListenAndServe(&quot;:3000&quot;, handler)

package objects

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



		var newobject Object
		_ = json.NewDecoder(r.Body).Decode(&amp;newobject )
         
        //Do things

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

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

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

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

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

		if resp.StatusCode &lt; 200 || resp.StatusCode &gt; 299 {
			w.WriteHeader(resp.StatusCode)
			w.Header().Set(&quot;Content-Type&quot;, &quot;application/json&quot;)
			w.Write(body)

		} else {
			w.WriteHeader(201)
		}


	
}

答案1

得分: 1

你有几个选择:

1)你可以定义一个接口,该接口具有http.Client的导出方法集。然后,你可以创建一个包级别的变量,该变量默认为http.Client类型。在CreateObject中,你将使用这个变量,而不是http.Client。由于它是一个接口,你可以轻松地模拟客户端。接口的定义如下:

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)
}

然而,由于你只调用Do()方法,你的模拟只需要为Do方法定义一个实际的测试实现。我们通常使用函数字段的方式来实现:

type MockClient struct {
    DoFunc func(req *http.Request) (*http.Response, error)
    // 其他函数字段,如果需要的话
}

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

// 将HTTPClient的其他4个方法定义为简单的返回值

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)在本地主机端口上搭建自己的HTTP模拟服务器,并在测试中将ExternalURL变量指向该服务器。这样可以实际测试拨号功能(使其更像是功能测试而不是单元测试),同时有效地“模拟”外部端点。

无论哪种情况,确保还编写一些回归测试,以确保外部端点仍按预期工作。

**编辑:**根据dm03514的建议,Go已经内置了一个模拟HTTP服务器:https://golang.org/pkg/net/http/httptest/

英文:

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(&quot;failed&quot;)
    },
}
  1. 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/

huangapple
  • 本文由 发表于 2017年6月24日 01:33:31
  • 转载请务必保留本文链接:https://go.coder-hub.com/44727059.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定