Go语言AWS Lambda单元测试:模拟外部函数复用失效问题求解
问题:Go语言AWS Lambda函数单元测试中模拟函数复用与回滚的问题
在为包含AWS Lambda的Go应用编写单元测试时,最初内联模拟NewAuthContextWithMap和LambdaInvoke的代码可以正常运行,但将模拟逻辑抽成单独函数后测试失效。原因是单独函数内的defer会在测试执行前就回滚模拟,导致实际调用目标函数时模拟已经失效。同时希望避免重复编写保存旧函数和回滚的代码。
初始可运行的内联模拟代码
func TestHandleLambdaEvent(t *testing.T) { ctx := context.TODO() oldNewAuthContextWithMap := NewAuthContextWithMap defer func() { NewAuthContextWithMap = oldNewAuthContextWithMap }() NewAuthContextWithMap = func(stringifiedMap map[string]interface{}) (*authutils.AuthContext, error) { return &authutils.AuthContext{UserID: "12345", Org: "XYZOrg", Role: "Member", Timestamp: 999999999}, nil } old := LambdaInvoke defer func() { LambdaInvoke = old }() LambdaInvoke = func(context context.Context, arn string, request, response interface{}) error { return nil } resp, err := handleLambdaEvent(ctx, events.APIGatewayProxyRequest{}) if err != nil { t.Fatalf("handleLambdaEvent returned error: %v", err) } if resp.StatusCode != http.StatusOK { t.Fatalf("Invalid status code, provided: %d required %d", resp.StatusCode, http.StatusOK) } }
抽离模拟函数后测试失效的代码
func mockNewAuthContextWithMap() { old := NewAuthContextWithMap defer func() { NewAuthContextWithMap = old }() NewAuthContextWithMap = func(stringifiedMap map[string]interface{}) (*authutils.AuthContext, error) { return &authutils.AuthContext{UserID: "12345", Org: "XYZOrg", Role: "Member", Timestamp: 999999999}, nil } }
调用该函数后测试失效,因为mockNewAuthContextWithMap执行完毕时,内部的defer就会立即回滚模拟函数,此时还没执行handleLambdaEvent。
尝试的手动回滚方案
func TestHandleLambdaEvent(t *testing.T) { ctx := context.TODO() mockNewAuthContextWithMap() mockLambdaInvoke() resp, err := handleLambdaEvent(ctx, events.APIGatewayProxyRequest{}) if err != nil { t.Fatalf("handleLambdaEvent returned error: %v", err) } if resp.StatusCode != http.StatusOK { t.Fatalf("Invalid status code, provided: %d required %d", resp.StatusCode, http.StatusOK) } defer RollbackExternalMethods() } func mockNewAuthContextWithMap() { NewAuthContextWithMap = func(stringifiedMap map[string]interface{}) (*authutils.AuthContext, error) { return &authutils.AuthContext{UserID: "12345", Org: "XYZOrg", Role: "Member", Timestamp: 999999999}, nil } } func mockLambdaInvoke() { LambdaInvoke = func(context context.Context, arn string, request, response interface{}) error { return nil } } func RollbackExternalMethods() { NewAuthContextWithMap = authutils.NewAuthContextWithMap LambdaInvoke = lambdaClient.Invoke }
目标函数handleLambdaEvent实现
func handleLambdaEvent(context context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { authContext, err := NewAuthContextWithMap(request.RequestContext.Authorizer) if err != nil { fmt.Println("Error parsing auth context:", err) return awsutils.StatusResponse(http.StatusInternalServerError), nil } queryRequest := handlerInput.GetProfile{ Type: handlerInput.TypeGetProfile, UserId: authContext.UserID, } queryResp := dbModel.User{} err = LambdaInvoke(context, userServiceArn, queryRequest, &queryResp) if err != nil { if ierrError, ok := err.(ierr.Error); ok { if ierrError.IsSame(user.RecordNotFoundError) { fmt.Printf("Could not find user profile of userId: %s \n", authContext.UserID) emptyResp := dbModel.User{} return awsutils.SwaggerResponse(http.StatusOK, emptyResp.SwaggerModel()), nil } } fmt.Println("Error invoking lambda:", err) return awsutils.StatusResponse(http.StatusInternalServerError), nil } swagUser := queryResp.SwaggerModel() return awsutils.SwaggerResponse(http.StatusOK, swagUser), nil }
解决方案
方案1:让模拟函数返回回滚函数
将每个模拟逻辑封装成返回func()的函数,在测试中调用模拟后,用defer执行回滚,这样回滚逻辑会在测试函数结束时执行,而非模拟函数结束时。
func mockNewAuthContextWithMap() func() { old := NewAuthContextWithMap NewAuthContextWithMap = func(stringifiedMap map[string]interface{}) (*authutils.AuthContext, error) { return &authutils.AuthContext{UserID: "12345", Org: "XYZOrg", Role: "Member", Timestamp: 999999999}, nil } // 返回回滚函数,由测试函数的defer触发 return func() { NewAuthContextWithMap = old } } func mockLambdaInvoke() func() { old := LambdaInvoke LambdaInvoke = func(context context.Context, arn string, request, response interface{}) error { return nil } return func() { LambdaInvoke = old } } // 测试函数中使用 func TestHandleLambdaEvent(t *testing.T) { ctx := context.TODO() // 调用模拟并立即用defer绑定回滚 defer mockNewAuthContextWithMap()() defer mockLambdaInvoke()() resp, err := handleLambdaEvent(ctx, events.APIGatewayProxyRequest{}) if err != nil { t.Fatalf("handleLambdaEvent returned error: %v", err) } if resp.StatusCode != http.StatusOK { t.Fatalf("Invalid status code, provided: %d required %d", resp.StatusCode, http.StatusOK) } }
方案2:封装测试工具类统一管理模拟与回滚
创建一个测试工具函数,统一管理所有需要模拟的函数,一次性完成模拟和回滚绑定,减少重复代码。
// 测试工具函数,返回统一的回滚函数 func SetupMocks() func() { oldNewAuth := NewAuthContextWithMap oldLambdaInvoke := LambdaInvoke // 设置模拟 NewAuthContextWithMap = func(stringifiedMap map[string]interface{}) (*authutils.AuthContext, error) { return &authutils.AuthContext{UserID: "12345", Org: "XYZOrg", Role: "Member", Timestamp: 999999999}, nil } LambdaInvoke = func(context context.Context, arn string, request, response interface{}) error { return nil } // 返回统一回滚函数 return func() { NewAuthContextWithMap = oldNewAuth LambdaInvoke = oldLambdaInvoke } } // 测试函数中使用 func TestHandleLambdaEvent(t *testing.T) { ctx := context.TODO() defer SetupMocks()() resp, err := handleLambdaEvent(ctx, events.APIGatewayProxyRequest{}) if err != nil { t.Fatalf("handleLambdaEvent returned error: %v", err) } if resp.StatusCode != http.StatusOK { t.Fatalf("Invalid status code, provided: %d required %d", resp.StatusCode, http.StatusOK) } }
方案3:依赖注入重构(推荐长期方案)
从根源上避免全局函数替换,通过依赖注入的方式将NewAuthContextWithMap和LambdaInvoke作为参数传入handleLambdaEvent,这样测试时直接传入模拟实现即可,无需修改全局变量,也不需要回滚。
首先重构handleLambdaEvent:
// 定义依赖结构体 type HandlerDependencies struct { NewAuthContext func(map[string]interface{}) (*authutils.AuthContext, error) InvokeLambda func(context.Context, string, interface{}, interface{}) error } // 改造handleLambdaEvent,接收依赖参数 func handleLambdaEvent(ctx context.Context, request events.APIGatewayProxyRequest, deps HandlerDependencies) (events.APIGatewayProxyResponse, error) { authContext, err := deps.NewAuthContext(request.RequestContext.Authorizer) if err != nil { fmt.Println("Error parsing auth context:", err) return awsutils.StatusResponse(http.StatusInternalServerError), nil } queryRequest := handlerInput.GetProfile{ Type: handlerInput.TypeGetProfile, UserId: authContext.UserID, } queryResp := dbModel.User{} err = deps.InvokeLambda(ctx, userServiceArn, queryRequest, &queryResp) if err != nil { if ierrError, ok := err.(ierr.Error); ok { if ierrError.IsSame(user.RecordNotFoundError) { fmt.Printf("Could not find user profile of userId: %s \n", authContext.UserID) emptyResp := dbModel.User{} return awsutils.SwaggerResponse(http.StatusOK, emptyResp.SwaggerModel()), nil } } fmt.Println("Error invoking lambda:", err) return awsutils.StatusResponse(http.StatusInternalServerError), nil } swagUser := queryResp.SwaggerModel() return awsutils.SwaggerResponse(http.StatusOK, swagUser), nil } // 生产环境调用时传入真实实现 func main() { // 示例:生产环境依赖 deps := HandlerDependencies{ NewAuthContext: authutils.NewAuthContextWithMap, InvokeLambda: lambdaClient.Invoke, } // 处理事件时传入deps // handleLambdaEvent(ctx, request, deps) }
然后测试时直接传入模拟实现:
func TestHandleLambdaEvent(t *testing.T) { ctx := context.TODO() // 定义模拟依赖 mockDeps := HandlerDependencies{ NewAuthContext: func(stringifiedMap map[string]interface{}) (*authutils.AuthContext, error) { return &authutils.AuthContext{UserID: "12345", Org: "XYZOrg", Role: "Member", Timestamp: 999999999}, nil }, InvokeLambda: func(context context.Context, arn string, request, response interface{}) error { return nil }, } resp, err := handleLambdaEvent(ctx, events.APIGatewayProxyRequest{}, mockDeps) if err != nil { t.Fatalf("handleLambdaEvent returned error: %v", err) } if resp.StatusCode != http.StatusOK { t.Fatalf("Invalid status code, provided: %d required %d", resp.StatusCode, http.StatusOK) } }
内容的提问来源于stack exchange,提问作者FrancMo
相关产品推荐
相关产品推荐

