英文:
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")
}
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论