使用httptest进行HTTP请求重试的单元测试

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

Unit-testing http request retry with httptest

问题

我正在努力为名为HttpRequest的组件编写单元测试,该组件封装了HTTP请求并处理响应的反序列化。最近,我为该组件添加了一个功能,允许在第一次尝试时遇到“连接被拒绝”错误时重新尝试HTTP请求。

要使用HttpRequest组件,我像这样调用它:user, err := HttpRequest[User](config)。config参数包含执行请求所需的所有信息,例如URL、方法、超时、重试次数和请求体。它还将响应体反序列化为指定类型的实例(在本例中为User)。

问题出现在我尝试测试初始请求失败并出现“连接被拒绝”错误,但第二次尝试成功的情况下。重试是在组件内部进行的,因此我只进行了一次对组件的调用。

我发现很难为这种情况创建单元测试,因为要使请求失败并出现“连接被拒绝”错误,需要在被调用的端口上没有监听器。问题在于,当使用httptest时,即使使用httptest.NewUnstartedServer,它在创建实例时仍会监听一个端口。因此,在创建httptest实例之后,我将永远不会在我的客户端代码中遇到“连接被拒绝”错误。

然而,在创建httptest实例之前,我不知道它将监听哪个端口。httptest总是选择一个随机端口,没有办法以编程方式指定端口。这意味着我不能在创建httptest实例之前进行HttpRequest调用。

有人对如何有效地对这种情况进行单元测试有任何想法吗?

英文:

I'm struggling to write a unit test for a component called HttpRequest that wraps HTTP requests and handles response unmarshalling. Recently, I added a feature to this component that allows it to retry an HTTP request if it encounters a "connection refused" error on the first attempt.

To use the HttpRequest component, I call it once like this: user, err := HttpRequest[User](config). The config parameter contains all the necessary information to execute the request, such as the URL, method, timeout, retry count, and request body. It also unmarshals the response body to an instance of the specified type (User in this case)

The problem arises when I try to test the scenario where the initial request fails with a "connection refused" error, but the second attempt is successful. The retries happen internally within the component, so I only make a single call to the component.

I've found it challenging to create a unit test for this scenario because, in order for a request to fail with "connection refused," there needs to be no listener on the port being called. The issue is that when using httptest, it always listens on a port when an instance is created, even with httptest.NewUnstartedServer. Consequently, after the httptest instance is created, I will never encounter a "connection refused" error in my client code.

However, before creating the httptest instance, I don't know which port it will be listening on. httptest always picks a random port, and there is no way to specify one programmatically. This means I can't make the HttpRequest call before creating the httptest instance.

Does anyone have any ideas on how to effectively unit test such a scenario?

答案1

得分: 1

NewUnstartedServer函数的代码如下:

func NewUnstartedServer(handler http.Handler) *Server {
	return &Server{
		Listener: newLocalListener(),
		Config:   &http.Server{Handler: handler},
	}
}

如果你想自己选择端口,可以按照以下方式实现:

func MyNewUnstartedServer(port int, handler http.Handler) *httptest.Server {
	addr := fmt.Sprintf("127.0.0.1:%d", port)
	l, err := net.Listen("tcp", addr)
	if err != nil {
		addr = fmt.Sprintf("[::1]::%d", port)
		if l, err = net.Listen("tcp6", addr); err != nil {
			panic(fmt.Sprintf("httptest: failed to listen on a port: %v", err))
		}
	}
	return &httptest.Server{
		Listener: l,
		Config:   &http.Server{Handler: handler},
	}
}

创建监听器的代码修改自httptest.newLocalListener


另一种选择是实现http.RoundTripper接口,并使用该RoundTripper创建一个http.Client。以下是从net/http/client_test.go中复制的示例代码:

type recordingTransport struct {
	req *Request
}

func (t *recordingTransport) RoundTrip(req *Request) (resp *Response, err error) {
	t.req = req
	return nil, errors.New("dummy impl")
}

func TestGetRequestFormat(t *testing.T) {
	setParallel(t)
	defer afterTest(t)
	tr := &recordingTransport{}
	client := &Client{Transport: tr}
	url := "http://dummy.faketld/"
	client.Get(url) // 注意:不会真正发送网络请求
	if tr.req.Method != "GET" {
		t.Errorf("expected method %q; got %q", "GET", tr.req.Method)
	}
	if tr.req.URL.String() != url {
		t.Errorf("expected URL %q; got %q", url, tr.req.URL.String())
	}
	if tr.req.Header == nil {
		t.Errorf("expected non-nil request Header")
	}
}
英文:

NewUnstartedServer is as simple as:

func NewUnstartedServer(handler http.Handler) *Server {
	return &Server{
		Listener: newLocalListener(),
		Config:   &http.Server{Handler: handler},
	}
}

If picking a port by yourself works for you, you can do it like this:

func MyNewUnstartedServer(port int, handler http.Handler) *httptest.Server {
	addr := fmt.Sprintf("127.0.0.1:%d", port)
	l, err := net.Listen("tcp", addr)
	if err != nil {
		addr = fmt.Sprintf("[::1]::%d", port)
		if l, err = net.Listen("tcp6", addr); err != nil {
			panic(fmt.Sprintf("httptest: failed to listen on a port: %v", err))
		}
	}
	return &httptest.Server{
		Listener: l,
		Config:   &http.Server{Handler: handler},
	}
}

The code to create a listener is modified from httptest.newLocalListener.


Another option is to implement the http.RoundTripper interface and create an http.Client with this RoundTripper. Here is an example copied from net/http/client_test.go:

type recordingTransport struct {
	req *Request
}

func (t *recordingTransport) RoundTrip(req *Request) (resp *Response, err error) {
	t.req = req
	return nil, errors.New("dummy impl")
}

func TestGetRequestFormat(t *testing.T) {
	setParallel(t)
	defer afterTest(t)
	tr := &recordingTransport{}
	client := &Client{Transport: tr}
	url := "http://dummy.faketld/"
	client.Get(url) // Note: doesn't hit network
	if tr.req.Method != "GET" {
		t.Errorf("expected method %q; got %q", "GET", tr.req.Method)
	}
	if tr.req.URL.String() != url {
		t.Errorf("expected URL %q; got %q", url, tr.req.URL.String())
	}
	if tr.req.Header == nil {
		t.Errorf("expected non-nil request Header")
	}
}

huangapple
  • 本文由 发表于 2023年5月26日 22:32:51
  • 转载请务必保留本文链接:https://go.coder-hub.com/76341798.html
匿名

发表评论

匿名网友

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

确定