You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.31 15:01:12