diff --git a/testutil.go b/testutil.go new file mode 100644 index 0000000..63af207 --- /dev/null +++ b/testutil.go @@ -0,0 +1,140 @@ +package touka + +import ( + "fmt" + "io" + "net/http" + "net/http/httptest" +) + +// CreateTestContext 为测试创建一个 *Context 和一个关联的 *Engine。 +// 它使用 httptest.NewRecorder() (如果传入的 w 为 nil) 来捕获响应。 +// 返回的 Context 已经过初始化,其 Writer 指向提供的 ResponseWriter (或新创建的 Recorder), +// 其 Request 是一个默认的 "GET /" 请求,并且其 engine 字段指向返回的 Engine 实例。 +// +// 参数: +// - w (http.ResponseWriter): 可选的。如果为 nil,函数内部会创建一个 httptest.ResponseRecorder。 +// 通常在测试中,你会传入一个 httptest.ResponseRecorder 来检查响应。 +// +// 返回: +// - c (*Context): 一个初始化的 Touka Context。 +// - r (*Engine): 一个新的 Touka Engine 实例,与 c 相关联。 +func CreateTestContext(w http.ResponseWriter) (c *Context, r *Engine) { + // 1. 如果未提供 ResponseWriter,则创建一个测试用的 Recorder + // ResponseRecorder 实现了 http.ResponseWriter 接口 + var testResponseWriter http.ResponseWriter = w + if testResponseWriter == nil { + testResponseWriter = httptest.NewRecorder() + } + + // 2. 创建一个新的 Engine 实例 + // 使用 New() 而不是 Default() 以获得一个“干净”的引擎,不带默认中间件 (如 Recovery) + // 如果你的测试依赖于 Default() 的中间件,可以改为 r = Default() + r = New() + + // 3. 从 Engine 的池中获取一个 Context 对象 + // 这是模拟真实请求处理的最佳方式 + c = r.pool.Get().(*Context) + + // 4. 创建一个默认的 HTTP 请求 + // 测试时可以根据需要修改这个请求的 Method, URL, Body, Headers 等 + // http.NewRequest 的 body 可以是 nil (对于GET) 或 bytes.NewBufferString("body content") 等 + req, err := http.NewRequest(http.MethodGet, "/", nil) + if err != nil { + // NewRequest 对于 "GET" 和 "/" 以及 nil body 通常不会失败 + // 但作为健壮性考虑,可以 panic 或返回错误 + panic("touka.CreateTestContext: Failed to create dummy request: " + err.Error()) // 英文 panic + } + // 确保请求有关联的 Context (尽管 c.reset 也会设置) + // req = req.WithContext(context.Background()) // 通常 reset 会处理这个 + + // 5. 重置/初始化 Context + // c.reset() 方法期望一个 http.ResponseWriter 和 *http.Request + // 它会将 c.Writer 包装成 touka.ResponseWriter (responseWriterImpl) + // 并设置 c.Request, c.Params (清空), c.handlers (nil), c.index (-1), + // c.Keys (新map), c.Errors (清空), c.ctx (来自 req.Context()), + // 以及 c.engine (在 c.pool.New 中已经设置,但 reset 会确保其他关联正确)。 + c.reset(testResponseWriter, req) + + // 确保 Context 中的 engine 字段指向我们创建的这个 Engine 实例 + // 虽然 c.pool.New 应该已经做了,但显式确认或设置无害,尤其是如果我们不完全依赖 pool.New 的细节。 + // 在当前的 Context.reset 实现中,c.engine 是在从池中 New() 时由 Engine 自身设置的, + // reset 方法不会改变它。所以只要 c 是从 r.pool 获取的,c.engine 就应该是 r。 + + return c, r +} + +// CreateTestContextWithRequest 功能与 CreateTestContext 类似,但允许传入自定义的 *http.Request。 +// 这对于测试需要特定请求方法、URL、头部或Body的处理器非常有用。 +// +// 参数: +// - w (http.ResponseWriter): 可选。如果为 nil,创建一个 httptest.ResponseRecorder。 +// - req (*http.Request): 用户提供的 HTTP 请求。如果为 nil,则内部创建一个默认的 "GET /"。 +// +// 返回: +// - c (*Context): 一个初始化的 Touka Context。 +// - r (*Engine): 一个新的 Touka Engine 实例,与 c 相关联。 +func CreateTestContextWithRequest(w http.ResponseWriter, req *http.Request) (c *Context, r *Engine) { + var testResponseWriter http.ResponseWriter = w + if testResponseWriter == nil { + testResponseWriter = httptest.NewRecorder() + } + + r = New() // 创建 Engine + c = r.pool.Get().(*Context) // 从池获取 Context + + var finalReq *http.Request = req + if finalReq == nil { // 如果未提供请求,创建默认请求 + var err error + finalReq, err = http.NewRequest(http.MethodGet, "/", nil) + if err != nil { + panic("touka.CreateTestContextWithRequest: Failed to create dummy request: " + err.Error()) // 英文 panic + } + } + + c.reset(testResponseWriter, finalReq) // 使用提供的或默认的请求重置 Context + + // c.engine 已由 r.pool.New 设置为 r + + return c, r +} + +// PerformRequest 在给定的 Engine 上执行一个模拟的 HTTP 请求,并返回响应记录器。 +// 这是一个更高级别的测试辅助函数,封装了创建请求、Context 和执行引擎的 ServeHTTP 方法。 +// +// 参数: +// - engine (*Engine): 要测试的 Touka 引擎实例。 +// - method (string): HTTP 请求方法 (例如 "GET", "POST")。 +// - path (string): 请求的路径 (例如 "/", "/users/123?name=test")。 +// - body (io.Reader): 可选的请求体。对于 GET, HEAD 等通常为 nil。 +// - headers (http.Header): 可选的请求头部。 +// +// 返回: +// - *httptest.ResponseRecorder: 包含响应状态、头部和主体的记录器。 +// +// 示例: +// +// rr := touka.PerformRequest(myEngine, "GET", "/ping", nil, nil) +// assert.Equal(t, http.StatusOK, rr.Code) +// assert.Equal(t, "pong", rr.Body.String()) +func PerformRequest(engine *Engine, method, path string, body io.Reader, headers http.Header) *httptest.ResponseRecorder { + req, err := http.NewRequest(method, path, body) + if err != nil { + // 通常 NewRequest 对于合法的方法和路径不会失败(除非路径解析错误) + panic(fmt.Sprintf("touka.PerformRequest: Failed to create request %s %s: %v", method, path, err)) // 英文 panic + } + + // 设置请求头部 (如果提供) + if headers != nil { + req.Header = headers + } + + // 创建一个 ResponseRecorder 来捕获响应 + rr := httptest.NewRecorder() + + // 直接调用 Engine 的 ServeHTTP 方法来处理请求 + // Engine 会负责创建和管理 Context + engine.ServeHTTP(rr, req) + + return rr +}