Golang中测试HTTP处理程序:使用服务器还是不使用服务器?

Golang中测试HTTP处理程序:使用服务器还是不使用服务器? 大家好,

我对测试 HTTP 处理程序感到困惑。我见过两种方法,想了解每种方法的优缺点。

例如,有以下自定义服务器:

package main

import (
	"context"
	"fmt"
	"io"
	"log"
	"net/http"
	"time"
)

const maxTimeout = 5 * time.Second

var server = &http.Server{}

func main() {
	server := setUpServer()

	fmt.Println("Server running...")

	err := server.ListenAndServe()
	if err != nil {
		log.Panic(err)
	}
}

func setUpServer() *http.Server {
	mux := http.NewServeMux()

	mux.Handle("/slow", http.TimeoutHandler(http.HandlerFunc(slowHandler), maxTimeout, "timeout"))

	server.Addr = ":4040"
	server.Handler = mux

	return server
}

func slowHandler(w http.ResponseWriter, r *http.Request) {
	data := slowOperation(r.Context())

	io.WriteString(w, data+"\n")
}

func slowOperation(ctx context.Context) string {
	fmt.Println("Working on it...")

	select {
	case <-ctx.Done():
		fmt.Println("Operation timed out.")

		return ""
	case <-time.After(10 * time.Second):
		return "data"
	}
}

可以像这样使用自定义服务器测试处理程序:

package main

import (
	"net/http"
	"net/http/httptest"
	"testing"
)

func TestServer(t *testing.T) {
	t.Run("slowHandler should return 503 if request timeouts", func(t *testing.T) {
		server := setUpServer()
		w := httptest.NewRecorder()
		r := httptest.NewRequest(http.MethodGet, "/slow", nil)

		server.Handler.ServeHTTP(w, r) // 使用自定义服务器

		expected := http.StatusServiceUnavailable
		got := w.Result().StatusCode

		if got != expected {
			t.Errorf("expected request to cause %v, got %v", expected, got)
		}
	})
}

但也可以像这样测试:

package main

import (
	"net/http"
	"net/http/httptest"
	"testing"
)

func TestServer(t *testing.T) {
	t.Run("slowHandler should return 503 if request timeouts", func(t *testing.T) {
		w := httptest.NewRecorder()
		r := httptest.NewRequest(http.MethodGet, "/slow", nil)

		slowHandler(w, r) // 直接使用处理程序

		expected := http.StatusServiceUnavailable
		got := w.Result().StatusCode

		if got != expected {
			t.Errorf("expected request to cause %v, got %v", expected, got)
		}
	})
}

在第一个测试中,我们会得到:

Working on it...
Operation timed out.
PASS
ok      test    5.451s

而在第二个测试中:

Working on it...
--- FAIL: TestServer (0.00s)
    --- FAIL: TestServer/slowHandler_should_return_503_if_request_timeouts (10.01s)
        main_test.go:39: expected request to cause 503, got 200
FAIL
exit status 1
FAIL    test    10.230s

因此,我们有第一个测试,它也测试了服务器返回正确输出的实现;而第二个测试纯粹测试处理程序失败,因为它不知道超时设置。此外,使用完全不同的请求(如 httptest.NewRequest(http.MethodDelete, "/", nil))也会产生相同的结果。

处理程序测试应该了解服务器吗?如果使用自定义服务器进行测试,会有什么缺点吗?


更多关于Golang中测试HTTP处理程序:使用服务器还是不使用服务器?的实战教程也可以访问 https://www.itying.com/category-94-b0.html

5 回复

谢谢,我会看看的!

更多关于Golang中测试HTTP处理程序:使用服务器还是不使用服务器?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


谢谢,但 http.Server 的超时设置并不适用于处理器:https://stackoverflow.com/a/51259258/8966651 此外,为了加速使用自定义服务器的测试,如果在每个测试中都实例化服务器,可以使用 t.Parallel()。

我的问题更多是关于使用自定义服务器进行测试是否是一种最佳实践。

我不是HTTP测试领域的专家,但我认为不使用自定义HTTP服务器会加快你的HTTP测试速度。不过,如果你还想测试服务器属性(即HTTP服务器结构体的属性,这类属性并不多),那么在你想测试Web服务器内部每个请求的ReadTimeoutWriteTimeoutMaxHeaderBytes等场景下,自定义服务器可能是有用的。

你不需要使用http.TimeoutHandler来为所有端点设置超时,HTTP服务器本身有默认的超时参数。我想不出比第二个例子更好的方法了,因为自定义服务器方法主要用于当你想要测试其结构体属性时。

在我看来,如果你想这样做,httptest.NewServer 可能是最佳选择。但这并不意味着你的设置是错误的,只是为了简洁起见。最重要的是,只要你不为代码库的一致性而同时使用两者,那就没问题。

为了提供更多信息,我找到了类似这样的内容。它主要讨论了一些关于模拟服务器的内容,但没有明确说明其优缺点。

gianarb blog

Go 语言中 httptest 包的强大之处

Go 语言测试之所以友好,原因之一在于核心团队已经提供了有用的测试包作为标准库的一部分,你可以直接使用,就像他们测试依赖这些包的代码一样。本文解释了如何…

在Golang中测试HTTP处理程序时,是否使用服务器取决于测试目标。以下是两种方法的详细对比:

1. 直接测试处理程序(不使用服务器)

这种方法直接调用处理函数,适合单元测试:

func TestSlowHandlerDirect(t *testing.T) {
    t.Run("handler returns 200 without timeout", func(t *testing.T) {
        w := httptest.NewRecorder()
        r := httptest.NewRequest(http.MethodGet, "/slow", nil)
        
        // 创建带取消的context来模拟超时
        ctx, cancel := context.WithTimeout(r.Context(), 100*time.Millisecond)
        defer cancel()
        r = r.WithContext(ctx)
        
        slowHandler(w, r)
        
        if w.Code != http.StatusOK {
            t.Errorf("expected 200, got %d", w.Code)
        }
    })
}

优点:

  • 测试隔离性好,只测试处理程序逻辑
  • 执行速度快
  • 可以精确控制输入参数

缺点:

  • 无法测试中间件(如TimeoutHandler)
  • 无法测试路由配置
  • 无法测试服务器级别的行为

2. 通过服务器测试(使用httptest.Server或自定义服务器)

使用httptest.Server:

func TestSlowHandlerWithTestServer(t *testing.T) {
    // 创建测试服务器
    mux := http.NewServeMux()
    mux.Handle("/slow", http.TimeoutHandler(
        http.HandlerFunc(slowHandler), 
        maxTimeout, 
        "timeout",
    ))
    
    ts := httptest.NewServer(mux)
    defer ts.Close()
    
    t.Run("request times out with 503", func(t *testing.T) {
        client := ts.Client()
        client.Timeout = maxTimeout
        
        resp, err := client.Get(ts.URL + "/slow")
        if err != nil {
            t.Fatal(err)
        }
        defer resp.Body.Close()
        
        if resp.StatusCode != http.StatusServiceUnavailable {
            t.Errorf("expected 503, got %d", resp.StatusCode)
        }
    })
}

使用自定义服务器(如你的示例):

func TestSlowHandlerWithCustomServer(t *testing.T) {
    t.Run("test timeout through server handler chain", func(t *testing.T) {
        server := setUpServer()
        w := httptest.NewRecorder()
        r := httptest.NewRequest(http.MethodGet, "/slow", nil)
        
        // 通过完整的处理链执行
        server.Handler.ServeHTTP(w, r)
        
        if w.Code != http.StatusServiceUnavailable {
            t.Errorf("expected 503, got %d", w.Code)
        }
    })
    
    t.Run("test other endpoints", func(t *testing.T) {
        server := setUpServer()
        w := httptest.NewRecorder()
        r := httptest.NewRequest(http.MethodGet, "/health", nil)
        
        // 测试其他路由
        server.Handler.ServeHTTP(w, r)
        
        // 验证响应
    })
}

优点:

  • 测试完整的请求处理链(中间件、路由等)
  • 更接近生产环境行为
  • 可以测试服务器配置(如超时设置)

缺点:

  • 执行速度较慢
  • 测试可能依赖外部配置
  • 测试失败时难以定位问题根源

3. 混合测试策略示例

在实际项目中,通常结合两种方法:

// 单元测试:测试处理程序核心逻辑
func TestSlowHandlerLogic(t *testing.T) {
    tests := []struct {
        name     string
        ctx      context.Context
        wantCode int
    }{
        {
            name:     "normal context",
            ctx:      context.Background(),
            wantCode: http.StatusOK,
        },
        {
            name:     "cancelled context",
            ctx:      func() context.Context {
                ctx, cancel := context.WithCancel(context.Background())
                cancel()
                return ctx
            }(),
            wantCode: http.StatusInternalServerError,
        },
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            w := httptest.NewRecorder()
            r := httptest.NewRequest(http.MethodGet, "/slow", nil)
            r = r.WithContext(tt.ctx)
            
            slowHandler(w, r)
            
            if w.Code != tt.wantCode {
                t.Errorf("got status %d, want %d", w.Code, tt.wantCode)
            }
        })
    }
}

// 集成测试:测试完整服务器行为
func TestServerIntegration(t *testing.T) {
    server := setUpServer()
    
    tests := []struct {
        name     string
        path     string
        method   string
        wantCode int
    }{
        {
            name:     "slow endpoint with timeout",
            path:     "/slow",
            method:   http.MethodGet,
            wantCode: http.StatusServiceUnavailable,
        },
        {
            name:     "non-existent endpoint",
            path:     "/not-found",
            method:   http.MethodGet,
            wantCode: http.StatusNotFound,
        },
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            w := httptest.NewRecorder()
            r := httptest.NewRequest(tt.method, tt.path, nil)
            
            server.Handler.ServeHTTP(w, r)
            
            if w.Code != tt.wantCode {
                t.Errorf("%s: got status %d, want %d", tt.name, w.Code, tt.wantCode)
            }
        })
    }
}

结论

在你的具体案例中,第二个测试失败是因为直接调用slowHandler绕过了TimeoutHandler中间件。处理程序本身没有设置503状态码,这个状态码是由超时中间件设置的。

建议:

  • 对处理程序的核心业务逻辑进行单元测试(不使用服务器)
  • 对包含中间件和路由的完整处理链进行集成测试(使用服务器)
  • 使用httptest.NewServer进行端到端测试,模拟真实HTTP请求

两种方法各有适用场景,通常在实际项目中会同时使用,以确保代码质量和测试覆盖率。

回到顶部