跳转至

使用本地模拟服务测试

httptest.Server 可以在本机模拟模型服务。测试不需要真实账号或外部网络,即可覆盖请求序列化、流式响应解析和最终消息组装。

下面的测试模拟 OpenAI Chat Completions 的 SSE 响应。将代码保存为使用 llm 的 Go package 中的 completion_test.go

完整测试

package myapp_test

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

    "github.com/ktsoator/or/llm"
    _ "github.com/ktsoator/or/llm/openai"
)

func TestCompletion(t *testing.T) {
    server := httptest.NewServer(http.HandlerFunc(
        func(w http.ResponseWriter, r *http.Request) {
            if r.URL.Path != "/v1/chat/completions" {
                t.Fatalf("path = %q", r.URL.Path)
            }
            w.Header().Set("Content-Type", "text/event-stream")
            io.WriteString(w, `data: {"choices":[{"index":0,"delta":{"content":"hi"},"finish_reason":null}]}`+"\n\n")
            io.WriteString(w, `data: {"choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}`+"\n\n")
            io.WriteString(w, "data: [DONE]\n\n")
        }))
    defer server.Close()

    model := llm.Model{
        ID: "test-model", Provider: "test",
        Protocol: llm.ProtocolOpenAICompletions,
        BaseURL: server.URL + "/v1", MaxTokens: 100,
    }
    message, err := llm.Complete(context.Background(), model,
        llm.Prompt("hello"), llm.StreamOptions{APIKey: "test"})
    if err != nil {
        t.Fatal(err)
    }
    if got := message.Text(); got != "hi" {
        t.Fatalf("text = %q, want hi", got)
    }
    if message.StopReason != llm.StopReasonStop {
        t.Fatalf("stop = %q", message.StopReason)
    }
}

运行:

go test ./...

示例中的 API 密钥只用于通过请求校验,并仅发送给本地测试服务器。

可以继续覆盖的路径

  • 读取请求 JSON,断言工具、系统指令、推理选项和请求头是否正确。
  • 第一次返回工具调用,第二次返回最终答案,覆盖完整工具调用流程。
  • 返回错误状态码或格式错误的 SSE,验证部分消息和错误处理。
  • 取消 Stream,并继续读取到通道关闭,检查 goroutine 是否能结束。
  • 测试自定义协议适配器或提供方覆盖时,使用自定义 Client 和独立注册表,避免包级全局状态影响其他测试。

测试范围

本地模拟测试验证应用逻辑和协议转换,不验证真实模型服务的鉴权、限流、字段差异或线上行为。生产使用的模型服务仍应保留少量带凭证的集成测试。