Golang中HTTP服务器的测试指南

Golang中HTTP服务器的测试指南 你好,我刚开始学习Go语言,正在寻求关于测试HTTP服务器实现的指导。

我想要测试的行为:

  • 在/mypath上收到请求
    • 方法必须是POST;所有其他方法返回405错误
      • 请求必须包含有效的JSON正文
        • 缺少正文发送422
        • 缺少必需字段发送422

这些行为是我自己编写的。可能我对RESTful最佳实践的理解有误。我想避免由于不准确的状态码而导致的API客户端实现困扰。我希望我的API用户能够快速理解他们的客户端出了什么问题,我相信准确的状态码将有助于这一点。

我的测试如下:

func TestServer(t *testing.T) {
	server := NewServer()

	t.Run("Send GET, expect 405", func(t *testing.T) {
		request, _ := http.NewRequest(http.MethodGet, "/myPath", nil)
		response := httptest.NewRecorder()

		server.ServeHTTP(response, request)
        assertResponseStatus(t, got, http.StatusMethodNotAllowed)
	})

	t.Run("Send POST without body, expect 422", func(t *testing.T) {
		var body = []byte(``)
		request, _ := http.NewRequest(http.MethodPost, "/myPath", nil)
		response := httptest.NewRecorder()

		request.Header.Set("Content-Type", JSONContentType)

		server.ServeHTTP(response, request)

        assertResponseStatus(t, got, http.StatusUnprocessableEntity)
	})
}

我意识到这里有一些重复,我还没有进行重构。我也没有为"所有其他方法返回405"编写测试,目前只有GET。我暂时不会涉及"缺少必需字段发送422"。

我正在使用gorilla/mux。

"方法必须是POST,所有其他方法返回405"通过以下方式实现:

router.Handle("/mypath", http.HandlerFunc(myPathHandler)).Methods("POST")

"发送没有正文的POST,期望422"是我遇到问题的地方。

如何构建一个空正文的请求?我尝试过:

request, _ := http.NewRequest(http.MethodPost, "/mypath", nil)

但在尝试时会导致空指针:

func myPathHandler(w http.ResponseWriter, r *http.Request) {
  myPathOpts := &myPathRequestBody{}

  err := json.NewDecoder(r.Body).Decode(myPathOpts)
  ...
}

所以我尝试:

func myPathHandler(w http.ResponseWriter, r *http.Request) {
    body, err := ioutil.ReadAll(r.Body)
	switch {
	case err == io.EOF:
		http.Error(w, err.Error(), http.StatusUnprocessableEntity)
	case err != nil:
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
    ...
}

但这也是一个空指针。

我以不同的方式构建请求:

request, _ := http.NewRequest(http.MethodPost, "/myPath", bytes.NewBuffer(nil))

或者

var body = []byte(``)
request, _ := http.NewRequest(http.MethodPost, "/myPath", bytes.NewBuffer(body))

但我没有得到io.EOF或err != nil。

添加一个switch case来测试body == nil:

func myPathHandler(w http.ResponseWriter, r *http.Request) {
	body, err := ioutil.ReadAll(r.Body)
	switch {
	case body == nil:
		http.Error(w, err.Error(), http.StatusUnprocessableEntity)
	case err == io.EOF:
		http.Error(w, err.Error(), http.StatusUnprocessableEntity)
	case err != nil:
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
    ...
}

那里也没有成功。

天啊,我的问题似乎有点冗长。感谢您的时间。

非常感谢您的指导和批评。

诚挚地


更多关于Golang中HTTP服务器的测试指南的实战教程也可以访问 https://www.itying.com/category-94-b0.html

3 回复

我不太确定,但422错误表示你期望特定的JSON结构,但实际却收到了空字符串或错误的JSON结构。比如你可能期望:

{"user": "Alex", "email": "alex@gmail.com"}

但却收到了空字符串或:

[1, 3, 44, "cat"]

虽然这是有效的JSON,但结构与你预期的不符。POST请求始终会有请求体,即使长度为零字节。

https://tools.ietf.org/html/rfc4918#section-11.2

422(无法处理的实体)状态码表示服务器理解请求实体的内容类型(因此不适合返回415不支持的媒体类型状态码),且请求实体的语法正确(因此不适合返回400错误请求状态码),但无法处理其中包含的指令。例如,当XML请求体包含格式正确(即语法正确)但语义错误的XML指令时,就可能出现此错误情况。

所以应该是:

更多关于Golang中HTTP服务器的测试指南的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


go version go1.11.2 linux/amd64

感谢您抽空回复。

我不理解为什么 err == io.EOF 不匹配。我阅读了源码中的注释,但显然遗漏了某些内容。我只是好奇我的理解偏差在哪里。

// Body 是请求的正文。 // // 对于客户端请求,nil 正文表示请求没有正文,例如 GET 请求。HTTP 客户端的传输负责调用 Close 方法。 // // 对于服务器请求,请求正文始终为非 nil,但在没有正文时会立即返回 EOF。服务器将关闭请求正文。ServeHTTP 处理程序不需要关闭。 Body io.ReadCloser

func myPathHandler(w http.ResponseWriter, r *http.Request) {
    body, err := ioutil.ReadAll(r.Body)
	switch {
	case err == io.EOF:
		http.Error(w, err.Error(), http.StatusUnprocessableEntity)
	case err != nil:
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
    ...
}

我通过检查 len(body) == 0 得到了想要的结果。

参见 使用空正文处理 POST 请求,返回 422 · GitHub

在代码片段中您会看到只有一个测试。这是带有显式空正文的测试。我无法让带有 nil 正文的测试运行而不出现空指针异常。

t.Run("Send POST with nil body, expect 422", func(t *testing.T) {
  request := newRequest(t, http.MethodPost, "/", nil)
  response := httptest.NewRecorder()

  server.ServeHTTP(response, request)

  got := response.Code

  assertResponseStatus(t, got, http.StatusUnprocessableEntity)

})

我想这只是一个糟糕的测试。

以下是针对您测试HTTP服务器实现中遇到问题的专业解答。

关于空请求体的处理

在Go中,使用http.NewRequest创建请求时,即使传入nil作为body,r.Body也不会是nil,而是一个空的io.ReadCloser。这就是为什么您的空指针检查失败的原因。

正确的空请求体测试方法:

t.Run("Send POST without body, expect 422", func(t *testing.T) {
    request, _ := http.NewRequest(http.MethodPost, "/myPath", bytes.NewReader(nil))
    request.Header.Set("Content-Type", "application/json")
    response := httptest.NewRecorder()

    server.ServeHTTP(response, request)
    
    assertResponseStatus(t, response.Code, http.StatusUnprocessableEntity)
})

改进的请求处理器实现

func myPathHandler(w http.ResponseWriter, r *http.Request) {
    // 检查请求体是否为空
    if r.Body == nil {
        http.Error(w, "Request body is required", http.StatusUnprocessableEntity)
        return
    }
    
    // 读取请求体内容
    bodyBytes, err := ioutil.ReadAll(r.Body)
    if err != nil {
        http.Error(w, "Failed to read request body", http.StatusInternalServerError)
        return
    }
    
    // 检查请求体是否为空
    if len(bodyBytes) == 0 {
        http.Error(w, "Request body cannot be empty", http.StatusUnprocessableEntity)
        return
    }
    
    // 解析JSON
    var requestBody myPathRequestBody
    if err := json.Unmarshal(bodyBytes, &requestBody); err != nil {
        http.Error(w, "Invalid JSON format", http.StatusUnprocessableEntity)
        return
    }
    
    // 继续处理有效请求...
}

完整的测试套件示例

func TestServer(t *testing.T) {
    server := NewServer()
    
    // 测试不支持的方法
    t.Run("Send GET, expect 405", func(t *testing.T) {
        request, _ := http.NewRequest(http.MethodGet, "/myPath", nil)
        response := httptest.NewRecorder()
        
        server.ServeHTTP(response, request)
        assertResponseStatus(t, response.Code, http.StatusMethodNotAllowed)
    })
    
    t.Run("Send PUT, expect 405", func(t *testing.T) {
        request, _ := http.NewRequest(http.MethodPut, "/myPath", nil)
        response := httptest.NewRecorder()
        
        server.ServeHTTP(response, request)
        assertResponseStatus(t, response.Code, http.StatusMethodNotAllowed)
    })
    
    // 测试空请求体
    t.Run("Send POST without body, expect 422", func(t *testing.T) {
        request, _ := http.NewRequest(http.MethodPost, "/myPath", bytes.NewReader(nil))
        request.Header.Set("Content-Type", "application/json")
        response := httptest.NewRecorder()
        
        server.ServeHTTP(response, request)
        assertResponseStatus(t, response.Code, http.StatusUnprocessableEntity)
    })
    
    // 测试无效JSON
    t.Run("Send POST with invalid JSON, expect 422", func(t *testing.T) {
        invalidJSON := []byte(`{"invalid": json}`)
        request, _ := http.NewRequest(http.MethodPost, "/myPath", bytes.NewReader(invalidJSON))
        request.Header.Set("Content-Type", "application/json")
        response := httptest.NewRecorder()
        
        server.ServeHTTP(response, request)
        assertResponseStatus(t, response.Code, http.StatusUnprocessableEntity)
    })
    
    // 测试缺少必需字段
    t.Run("Send POST with missing required field, expect 422", func(t *testing.T) {
        incompleteJSON := []byte(`{"optionalField": "value"}`)
        request, _ := http.NewRequest(http.MethodPost, "/myPath", bytes.NewReader(incompleteJSON))
        request.Header.Set("Content-Type", "application/json")
        response := httptest.NewRecorder()
        
        server.ServeHTTP(response, request)
        assertResponseStatus(t, response.Code, http.StatusUnprocessableEntity)
    })
}

func assertResponseStatus(t *testing.T, got, want int) {
    t.Helper()
    if got != want {
        t.Errorf("got status %d, want %d", got, want)
    }
}

使用gorilla/mux的路由配置

func NewServer() *mux.Router {
    router := mux.NewRouter()
    router.Handle("/mypath", http.HandlerFunc(myPathHandler)).Methods("POST")
    return router
}

请求体结构定义

type myPathRequestBody struct {
    RequiredField string `json:"requiredField"`
    OptionalField string `json:"optionalField,omitempty"`
}

您对状态码的选择是正确的。422 Unprocessable Entity适用于语义错误(如缺少必需字段),而400 Bad Request适用于语法错误。这种区分确实有助于API客户端更好地理解问题所在。

回到顶部