Eino集成测试:端到端测试方案
【免费下载链接】eino 项目地址: https://gitcode.com/GitHub_Trending/ei/eino
概述
在LLM(Large Language Model,大语言模型)应用开发中,集成测试是确保系统各组件协同工作的关键环节。Eino作为Go语言的LLM应用开发框架,提供了强大的编排能力和组件抽象,但同时也带来了测试复杂性的挑战。本文将深入探讨Eino项目的集成测试策略,提供端到端的测试解决方案。
Eino测试架构解析
测试层次结构
Eino项目采用分层测试策略,确保从单元到集成的全面覆盖:
核心测试组件
Eino的测试架构围绕以下几个核心概念构建:
| 测试类型 | 测试目标 | 技术实现 |
|---|---|---|
| 组件测试 | 验证单个组件功能 | Mock对象 + 接口测试 |
| Graph测试 | 验证编排逻辑 | 真实组件 + 模拟数据 |
| 流程测试 | 验证端到端流程 | 完整组件链 + 真实数据 |
| 回调测试 | 验证切面逻辑 | Callback Handler注入 |
集成测试环境搭建
测试依赖配置
在开始集成测试前,需要配置适当的测试环境:
// go.mod 测试依赖配置
require (
github.com/stretchr/testify v1.9.0 // 断言库
go.uber.org/mock v0.4.0 // Mock框架
github.com/smartystreets/goconvey v1.8.1 // 测试工具
)
Mock组件实现
Eino提供了丰富的Mock组件,用于隔离测试:
// 使用Mock ChatModel进行测试
mockModel := &model.ChatModelMock{
GenerateFunc: func(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.Message, error) {
return &schema.Message{
Role: schema.Assistant,
Content: "Mock response for testing",
}, nil
},
StreamFunc: func(ctx context.Context, input []*schema.Message, opts ...model.Option) (*schema.StreamReader[*schema.Message], error) {
// 实现流式响应模拟
},
}
Graph编排测试策略
基础Graph测试用例
func TestSingleGraph(t *testing.T) {
const (
nodeOfModel = "model"
nodeOfPrompt = "prompt"
)
ctx := context.Background()
g := NewGraph[map[string]any, *schema.Message]()
// 创建Prompt模板节点
pt := prompt.FromMessages(schema.FString,
schema.UserMessage("what's the weather in {location}?"),
)
err := g.AddChatTemplateNode("prompt", pt)
assert.NoError(t, err)
// 创建Mock ChatModel节点
cm := &chatModel{
msgs: []*schema.Message{
{
Role: schema.Assistant,
Content: "the weather is good",
},
},
}
err = g.AddChatModelNode(nodeOfModel, cm, WithNodeName("MockChatModel"))
assert.NoError(t, err)
// 构建Graph连接
err = g.AddEdge(START, nodeOfPrompt)
assert.NoError(t, err)
err = g.AddEdge(nodeOfPrompt, nodeOfModel)
assert.NoError(t, err)
err = g.AddEdge(nodeOfModel, END)
assert.NoError(t, err)
// 编译并执行Graph
r, err := g.Compile(context.Background(), WithMaxRunSteps(10))
assert.NoError(t, err)
// 测试不同输入场景
testCases := []struct {
name string
input map[string]any
expected string
hasError bool
}{
{"正常输入", map[string]any{"location": "beijing"}, "the weather is good", false},
{"错误输入", map[string]any{"wrong key": 1}, "", true},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result, err := r.Invoke(ctx, tc.input)
if tc.hasError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tc.expected, result.Content)
}
})
}
}
流式处理测试
Eino支持完整的流式处理,测试时需要验证各种流式场景:
func TestStreamProcessing(t *testing.T) {
// 测试流式Invoke
s, err := r.Stream(ctx, in)
assert.NoError(t, err)
// 验证流式数据收集
result, err := concatStreamReader(s)
assert.NoError(t, err)
assert.Contains(t, result, "expected content")
// 测试Transform模式
sr, sw := schema.Pipe[map[string]any](1)
_ = sw.Send(in, nil)
sw.Close()
transformed, err := r.Transform(ctx, sr)
assert.NoError(t, err)
// 验证流式转换
finalResult, err := concatStreamReader(transformed)
assert.NoError(t, err)
}
嵌套Graph测试方案
多层级Graph集成
func TestNestedGraph(t *testing.T) {
const (
nodeOfLambda1 = "lambda1"
nodeOfLambda2 = "lambda2"
nodeOfSubGraph = "sub_graph"
)
ctx := context.Background()
g := NewGraph[string, *schema.Message]()
// 创建子Graph
sg := NewGraph[map[string]any, *schema.Message]()
// 配置子Graph组件
pt := prompt.FromMessages(schema.FString,
schema.UserMessage("what's the weather in {location}?"),
)
err := sg.AddChatTemplateNode("prompt", pt)
assert.NoError(t, err)
cm := &chatModel{
msgs: []*schema.Message{
{Role: schema.Assistant, Content: "the weather is good"},
},
}
err = sg.AddChatModelNode("model", cm, WithNodeName("MockChatModel"))
assert.NoError(t, err)
// 构建子Graph连接
err = sg.AddEdge(START, "prompt")
assert.NoError(t, err)
err = sg.AddEdge("prompt", "model")
assert.NoError(t, err)
err = sg.AddEdge("model", END)
assert.NoError(t, err)
// 在主Graph中集成子Graph
l1 := InvokableLambda[string, map[string]any](
func(ctx context.Context, input string) (output map[string]any, err error) {
return map[string]any{"location": input}, nil
})
l2 := InvokableLambda[*schema.Message, *schema.Message](
func(ctx context.Context, input *schema.Message) (output *schema.Message, err error) {
input.Content = fmt.Sprintf("processed: %s", input.Content)
return input, nil
})
err = g.AddLambdaNode(nodeOfLambda1, l1, WithNodeName("Lambda1"))
assert.NoError(t, err)
err = g.AddGraphNode(nodeOfSubGraph, sg, WithNodeName("SubGraphName"))
assert.NoError(t, err)
err = g.AddLambdaNode(nodeOfLambda2, l2, WithNodeName("Lambda2"))
assert.NoError(t, err)
// 构建完整数据流
err = g.AddEdge(START, nodeOfLambda1)
assert.NoError(t, err)
err = g.AddEdge(nodeOfLambda1, nodeOfSubGraph)
assert.NoError(t, err)
err = g.AddEdge(nodeOfSubGraph, nodeOfLambda2)
assert.NoError(t, err)
err = g.AddEdge(nodeOfLambda2, END)
assert.NoError(t, err)
// 编译和执行
r, err := g.Compile(context.Background(), WithMaxRunSteps(10))
assert.NoError(t, err)
// 验证嵌套Graph功能
result, err := r.Invoke(ctx, "london")
assert.NoError(t, err)
assert.Equal(t, "processed: the weather is good", result.Content)
}
回调机制测试
Callback注入测试
Eino的切面机制(Aspect)通过Callback实现,测试时需要验证回调的正确触发:
func TestCallbackInjection(t *testing.T) {
ck := "callback_depth"
cb := callbacks.NewHandlerBuilder().
OnStartFn(func(ctx context.Context, info *callbacks.RunInfo, input callbacks.CallbackInput) context.Context {
// 记录回调深度
v, ok := ctx.Value(ck).(int)
if ok {
v++
}
return context.WithValue(ctx, ck, v)
}).
OnStartWithStreamInputFn(func(ctx context.Context, info *callbacks.RunInfo, input *schema.StreamReader[callbacks.CallbackInput]) context.Context {
input.Close()
v, ok := ctx.Value(ck).(int)
if ok {
v++
}
return context.WithValue(ctx, ck, v)
}).Build()
// 测试各种执行模式下的回调
testModes := []struct {
name string
testFunc func() error
}{
{"Invoke模式", func() error {
_, err := r.Invoke(ctx, "london", WithCallbacks(cb))
return err
}},
{"Stream模式", func() error {
rs, err := r.Stream(ctx, "london", WithCallbacks(cb))
if err != nil {
return err
}
for {
_, err = rs.Recv()
if err == io.EOF {
break
}
if err != nil {
return err
}
}
return nil
}},
}
for _, mode := range testModes {
t.Run(mode.name, func(t *testing.T) {
err := mode.testFunc()
assert.NoError(t, err)
// 验证回调被正确触发
depth := ctx.Value(ck).(int)
assert.Greater(t, depth, 0)
})
}
}
类型安全测试
运行时类型检查
Eino在Graph编排时进行编译期类型检查,但运行时仍需验证类型安全:
func TestRuntimeTypeSafety(t *testing.T) {
// 测试Any类型的运行时检查
anyG := NewGraph[any, string]()
err := anyG.AddLambdaNode("node1", InvokableLambda(func(ctx context.Context, input string) (output any, err error) {
return input + "node1", nil
}))
assert.NoError(t, err)
err = anyG.AddLambdaNode("node2", InvokableLambda(func(ctx context.Context, input string) (output any, err error) {
return input + "node2", nil
}))
assert.NoError(t, err)
// 构建Graph
err = anyG.AddEdge(START, "node1")
assert.NoError(t, err)
err = anyG.AddEdge("node1", "node2")
assert.NoError(t, err)
err = anyG.AddEdge("node2", END)
assert.NoError(t, err)
r, err := anyG.Compile(context.Background())
assert.NoError(t, err)
// 测试正常类型
result, err := r.Invoke(context.Background(), "start")
assert.NoError(t, err)
assert.Equal(t, "startnode1node2", result)
// 测试类型错误场景
anyG = NewGraph[any, string]()
err = anyG.AddLambdaNode("node1", InvokableLambda(func(ctx context.Context, input string) (output any, err error) {
return 123 // 返回错误类型
}))
assert.NoError(t, err)
err = anyG.AddLambdaNode("node2", InvokableLambda(func(ctx context.Context, input string) (output any, err error) {
return input + "node2", nil
}))
assert.NoError(t, err)
// 构建并测试类型错误
err = anyG.AddEdge(START, "node1")
assert.NoError(t, err)
err = anyG.AddEdge("node1", "node2")
assert.NoError(t, err)
err = anyG.AddEdge("node2", END)
assert.NoError(t, err)
r, err = anyG.Compile(context.Background())
assert.NoError(t, err)
_, err = r.Invoke(context.Background(), "start")
assert.Error(t, err)
assert.Contains(t, err.Error(), "runtime type check")
}
分支逻辑测试
条件分支测试
Eino支持基于条件的动态分支,测试时需要覆盖各种分支场景:
func TestConditionalBranches(t *testing.T) {
g := NewGraph[string, string]()
// 创建分支节点
err := g.AddLambdaNode("node1", InvokableLambda(func(ctx context.Context, input string) (output any, err error) {
return input + "node1", nil
}))
assert.NoError(t, err)
// 定义分支条件
branch := NewGraphBranch(func(ctx context.Context, in string) (endNode string, err error) {
if len(in) > 5 {
return "node2", nil
}
return "node3", nil
}, map[string]bool{"node2": true, "node3": true})
err = g.AddBranch("node1", branch)
assert.NoError(t, err)
// 创建不同分支的处理节点
err = g.AddLambdaNode("node2", InvokableLambda(func(ctx context.Context, input string) (output any, err error) {
return "long_branch", nil
}))
assert.NoError(t, err)
err = g.AddLambdaNode("node3", InvokableLambda(func(ctx context.Context, input string) (output any, err error) {
return "short_branch", nil
}))
assert.NoError(t, err)
// 构建Graph
err = g.AddEdge(START, "node1")
assert.NoError(t, err)
err = g.AddEdge("node2", END)
assert.NoError(t, err)
err = g.AddEdge("node3", END)
assert.NoError(t, err)
r, err := g.Compile(context.Background())
assert.NoError(t, err)
// 测试不同分支条件
testCases := []struct {
input string
expected string
}{
{"short", "short_branch"},
{"very_long_input", "long_branch"},
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("Input: %s", tc.input), func(t *testing.T) {
result, err := r.Invoke(context.Background(), tc.input)
assert.NoError(t, err)
assert.Equal(t, tc.expected, result)
})
}
}
性能与并发测试
并发安全测试
Eino的State处理需要保证并发安全:
func TestConcurrencySafety(t *testing.T) {
g := NewGraph[string, int]()
// 创建有状态的操作节点
err := g.AddLambdaNode("counter", InvokableLambda(func(ctx context.Context, input string) (output int, err error) {
state := GetState(ctx)
state.Mutex.Lock()
defer state.Mutex.Unlock()
state.Counter++
return state.Counter, nil
}))
assert.NoError(t, err)
err = g.AddEdge(START, "counter")
assert.NoError(t, err)
err = g.AddEdge("counter", END)
assert.NoError(t, err)
r, err := g.Compile(context.Background())
assert.NoError(t, err)
// 并发测试
const concurrentRequests = 100
results := make(chan int, concurrentRequests)
errors := make(chan error, concurrentRequests)
var wg sync.WaitGroup
for i := 0; i < concurrentRequests; i++ {
wg.Add(1)
go func() {
defer wg.Done()
result, err := r.Invoke(context.Background(), "test")
if err != nil {
errors <- err
return
}
results <- result
}()
}
wg.Wait()
close(results)
close(errors)
// 验证没有错误且结果唯一
assert.Empty(t, errors)
uniqueResults := make(map[int]bool)
for result := range results {
uniqueResults[result] = true
}
assert.Len(t, uniqueResults, concurrentRequests)
}
测试最佳实践
测试组织结构
建议采用以下测试目录结构:
test/
├── integration/ # 集成测试
│ ├── graph/ # Graph编排测试
│ ├── components/ # 组件集成测试
│ └── workflow/ # 工作流测试
├── performance/ # 性能测试
└── fixtures/ # 测试数据
测试数据管理
使用Table-Driven Tests(表格驱动测试)组织测试用例:
func TestGraphScenarios(t *testing.T) {
testCases := []struct {
name string
setupGraph func() *Graph[any, any]
input any
expected any
shouldError bool
}{
{
name: "简单Chat流程",
setupGraph: func() *Graph[any, any] {
g := NewGraph[any, any]()
// Graph设置逻辑
return g
},
input: map[string]any{"query": "Hello"},
expected: "Response",
shouldError: false,
},
// 更多测试场景...
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
g := tc.setupGraph()
r, err := g.Compile(context.Background())
if tc.shouldError {
assert.Error(t, err)
return
}
assert.NoError(t, err)
result, err := r.Invoke(context.Background(), tc.input)
assert.NoError(t, err)
assert.Equal(t, tc.expected, result)
})
}
}
总结
Eino的集成测试需要全面覆盖Graph编排、流式处理、类型安全、回调机制等核心功能。通过本文提供的测试方案,您可以:
- 构建可靠的测试基础设施:使用Mock组件和测试工具
- 验证复杂编排逻辑:测试嵌套Graph和条件分支
- 确保类型安全:运行时类型检查和验证
- 测试并发场景:验证State处理的线程安全性
- 覆盖全流程:从单元测试到端到端集成测试
遵循这些测试实践,将帮助您构建高质量、可靠的Eino LLM应用程序,确保在生产环境中的稳定性和性能。
测试是质量的守护者,在LLM应用开发中更是如此。通过全面的集成测试,我们不仅验证代码正确性,更确保AI应用的可预测性和可靠性。
【免费下载链接】eino 项目地址: https://gitcode.com/GitHub_Trending/ei/eino
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



