Golang中如何测试HTTP处理程序

Golang中如何测试HTTP处理程序 我尝试测试我的处理器是否返回了正确的HTML模板,我通过“字符串化”实现了这一点,这个方法似乎通过了测试。以下是我的问题和困惑:

  1. 是否有更好的设计方法来实现这个?
  2. 这个方法在我传递动态数据的模板上失败了

template.Execute(a.html, passed-in-data)

有什么更好的方法可以做到这一点吗?谢谢。

4 回复

有什么更好的方法可以做到这一点?

这里是我创建“静态网站”的方法

Go Playground - The Go Programming Language

更多关于Golang中如何测试HTTP处理程序的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


package http_test

import (
	"io/fs"
	"net/http"
	"net/http/httptest"
	"testing"

	handlers "01.kood.tech/git/AmrKharaba/ascii-art/http"
	pages "01.kood.tech/git/AmrKharaba/ascii-art/templates"
)

func AssertEqual[T comparable](t *testing.T, actual, expected T) {
	t.Helper()

	if actual != expected {
		t.Errorf("\ngot %v;\n\n\n\n\n\n\n expected %v\n\n", actual, expected)
	}
}

此文件已被截断。显示原文

你好 @Oluwatobi_Giwa

你能分享更多关于你问题的细节吗?没有上下文,很难理解你想要实现什么,或者你的代码为什么会失败。

  • “stringing-fy”是什么?你为什么需要这个来让你的处理程序响应正确的HTML模板?
  • 在“有没有更好的方法来处理这个问题?”中,“this”指的是什么?
  • Execute方法是如何失败的?特别是:
    • 你收到了什么错误信息?
    • 你使用了哪个模板?
    • 你传入了什么数据?
    • Execute方法对于其他类型的数据或模板是否成功?如果是,成功的情况与失败的情况有何不同?

如果可能,请在这里分享一个能重现问题的最简版本的代码和数据。你可以使用 Go Playground 来分享可执行的代码。

对于测试HTTP处理程序,特别是涉及HTML模板渲染的场景,有几种更健壮的方法。以下是针对你问题的具体解决方案:

1. 使用httptest包进行集成测试

package main

import (
    "net/http"
    "net/http/httptest"
    "testing"
    "html/template"
    "strings"
)

func TestHandlerReturnsCorrectTemplate(t *testing.T) {
    // 创建模板
    tmpl := template.Must(template.New("test").Parse(`
        <html>
            <body>
                <h1>{{.Title}}</h1>
                <p>{{.Message}}</p>
            </body>
        </html>
    `))

    // 创建处理器
    handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        data := struct {
            Title   string
            Message string
        }{
            Title:   "Test Page",
            Message: "Dynamic Content",
        }
        tmpl.Execute(w, data)
    })

    // 创建测试请求
    req := httptest.NewRequest("GET", "/", nil)
    rr := httptest.NewRecorder()

    // 执行请求
    handler.ServeHTTP(rr, req)

    // 验证响应
    if status := rr.Code; status != http.StatusOK {
        t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
    }

    // 检查动态内容
    expected := "Test Page"
    if !strings.Contains(rr.Body.String(), expected) {
        t.Errorf("handler returned unexpected body: got %v want to contain %v", rr.Body.String(), expected)
    }

    // 检查另一个动态字段
    expectedMessage := "Dynamic Content"
    if !strings.Contains(rr.Body.String(), expectedMessage) {
        t.Errorf("handler returned unexpected body: got %v want to contain %v", rr.Body.String(), expectedMessage)
    }
}

2. 使用模板解析器进行单元测试

package main

import (
    "bytes"
    "html/template"
    "testing"
)

func TestTemplateRendering(t *testing.T) {
    // 定义模板
    tmplStr := `
        <div>
            <h2>{{.Name}}</h2>
            <p>Age: {{.Age}}</p>
            {{range .Items}}
                <li>{{.}}</li>
            {{end}}
        </div>
    `

    // 解析模板
    tmpl, err := template.New("test").Parse(tmplStr)
    if err != nil {
        t.Fatalf("Failed to parse template: %v", err)
    }

    // 测试数据
    data := struct {
        Name  string
        Age   int
        Items []string
    }{
        Name:  "John Doe",
        Age:   30,
        Items: []string{"Item1", "Item2", "Item3"},
    }

    // 执行模板到缓冲区
    var buf bytes.Buffer
    err = tmpl.Execute(&buf, data)
    if err != nil {
        t.Fatalf("Failed to execute template: %v", err)
    }

    // 验证输出
    result := buf.String()
    
    // 检查动态内容
    checks := []string{
        "John Doe",
        "Age: 30",
        "<li>Item1</li>",
        "<li>Item2</li>",
        "<li>Item3</li>",
    }

    for _, check := range checks {
        if !strings.Contains(result, check) {
            t.Errorf("Template missing expected content: %s", check)
        }
    }
}

3. 完整的HTTP处理器测试示例

package main

import (
    "net/http"
    "net/http/httptest"
    "testing"
    "html/template"
    "io/ioutil"
)

// 处理器函数
func UserProfileHandler(tmpl *template.Template) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        userID := r.URL.Query().Get("id")
        
        data := map[string]interface{}{
            "UserID":   userID,
            "Username": "user_" + userID,
            "Email":    "user" + userID + "@example.com",
            "Roles":    []string{"admin", "editor"},
        }
        
        w.Header().Set("Content-Type", "text/html; charset=utf-8")
        tmpl.Execute(w, data)
    }
}

func TestUserProfileHandler(t *testing.T) {
    // 创建模板
    tmpl := template.Must(template.New("profile").Parse(`
        <!DOCTYPE html>
        <html>
        <head>
            <title>Profile {{.UserID}}</title>
        </head>
        <body>
            <h1>User: {{.Username}}</h1>
            <p>Email: {{.Email}}</p>
            <ul>
                {{range .Roles}}
                <li>{{.}}</li>
                {{end}}
            </ul>
        </body>
        </html>
    `))

    // 创建处理器
    handler := UserProfileHandler(tmpl)

    // 测试用例
    tests := []struct {
        name           string
        userID         string
        expectedStatus int
        expectedInBody []string
    }{
        {
            name:           "valid user id",
            userID:         "123",
            expectedStatus: http.StatusOK,
            expectedInBody: []string{
                "Profile 123",
                "user_123",
                "user123@example.com",
                "<li>admin</li>",
                "<li>editor</li>",
            },
        },
        {
            name:           "another user id",
            userID:         "456",
            expectedStatus: http.StatusOK,
            expectedInBody: []string{
                "Profile 456",
                "user_456",
                "user456@example.com",
            },
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            // 创建请求
            req := httptest.NewRequest("GET", "/profile?id="+tt.userID, nil)
            rr := httptest.NewRecorder()

            // 执行请求
            handler.ServeHTTP(rr, req)

            // 验证状态码
            if rr.Code != tt.expectedStatus {
                t.Errorf("expected status %d, got %d", tt.expectedStatus, rr.Code)
            }

            // 读取响应体
            body, _ := ioutil.ReadAll(rr.Body)
            bodyStr := string(body)

            // 验证内容
            for _, expected := range tt.expectedInBody {
                if !strings.Contains(bodyStr, expected) {
                    t.Errorf("expected body to contain %q, but it didn't", expected)
                }
            }

            // 验证Content-Type
            contentType := rr.Header().Get("Content-Type")
            expectedContentType := "text/html; charset=utf-8"
            if contentType != expectedContentType {
                t.Errorf("expected Content-Type %q, got %q", expectedContentType, contentType)
            }
        })
    }
}

4. 使用golden文件进行模板输出验证

package main

import (
    "bytes"
    "html/template"
    "io/ioutil"
    "path/filepath"
    "testing"
)

func TestTemplateWithGoldenFile(t *testing.T) {
    tmpl := template.Must(template.New("test").Parse(`
        <table>
            <tr>
                <th>Product</th>
                <th>Price</th>
                <th>Stock</th>
            </tr>
            {{range .Products}}
            <tr>
                <td>{{.Name}}</td>
                <td>${{.Price}}</td>
                <td>{{.Stock}} units</td>
            </tr>
            {{end}}
        </table>
    `))

    data := struct {
        Products []Product
    }{
        Products: []Product{
            {Name: "Laptop", Price: 999.99, Stock: 50},
            {Name: "Mouse", Price: 29.99, Stock: 200},
            {Name: "Keyboard", Price: 89.99, Stock: 75},
        },
    }

    var buf bytes.Buffer
    err := tmpl.Execute(&buf, data)
    if err != nil {
        t.Fatal(err)
    }

    // 获取golden文件路径
    goldenPath := filepath.Join("testdata", "expected_output.html")
    
    // 更新golden文件(仅在需要时取消注释)
    // ioutil.WriteFile(goldenPath, buf.Bytes(), 0644)

    // 读取期望的输出
    expected, err := ioutil.ReadFile(goldenPath)
    if err != nil {
        t.Fatal(err)
    }

    if !bytes.Equal(buf.Bytes(), expected) {
        t.Errorf("template output doesn't match golden file")
        t.Logf("Got:\n%s", buf.String())
        t.Logf("Expected:\n%s", string(expected))
    }
}

type Product struct {
    Name  string
    Price float64
    Stock int
}

这些方法提供了更可靠的测试策略:

  1. 使用httptest进行完整的HTTP栈测试
  2. 直接测试模板渲染逻辑
  3. 验证动态数据是否正确注入
  4. 通过golden文件确保输出一致性

对于动态数据模板,重点在于验证模板执行后的输出是否包含预期的动态内容,而不是简单比较整个HTML字符串。

回到顶部