AWS SDK Go Lambda 单元测试

时间:2020-12-27 09:02:41

标签: amazon-web-services unit-testing go aws-lambda

我有史以来第一次尝试编写一些单元测试,我正在 golang 中为一个使用 aws lambda 的副项目做这件事。

下面是两个文件。

main.go 获取包含电子邮件地址的事件,并在认知用户池中创建用户。

main_test.go 应该模拟 createUser 中的 main.go 函数,但是当我尝试运行测试时出现错误。

在观看 instantiating the client globally 后,我刚刚将代码从 this youtube video 切换为在 aws sdk 接口上使用指针接收器方法。

ma​​in.go

package main

import (
    "fmt"
    "log"
    "os"

    "github.com/aws/aws-lambda-go/events"
    "github.com/aws/aws-lambda-go/lambda"
    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/awserr"
    "github.com/aws/aws-sdk-go/aws/session"
    cidp "github.com/aws/aws-sdk-go/service/cognitoidentityprovider"
    cidpif "github.com/aws/aws-sdk-go/service/cognitoidentityprovider/cognitoidentityprovideriface"
    lib "github.com/sean/repo/lib"
)

type createUserEvent struct {
    EmailAddress string `json:"email_address"`
}

type awsService struct {
    cidpif.CognitoIdentityProviderAPI
}

func (c *awsService) createUser(e createUserEvent) error {
    input := &cidp.AdminCreateUserInput{
        UserPoolId:             aws.String(os.Getenv("USER_POOL_ID")),
        Username:               aws.String(e.EmailAddress),
        DesiredDeliveryMediums: []*string{aws.String("EMAIL")},
        ForceAliasCreation:     aws.Bool(true),
        UserAttributes: []*cidp.AttributeType{
            {
                Name:  aws.String("email"),
                Value: aws.String(e.EmailAddress),
            },
        },
    }
    _, err := c.AdminCreateUser(input)
    if err != nil {
        if aerr, ok := err.(awserr.Error); ok {
            switch aerr.Code() {
            case cidp.ErrCodeResourceNotFoundException:
                log.Printf("[ERROR] %v, %v", cidp.ErrCodeResourceNotFoundException, aerr.Error())
            case cidp.ErrCodeInvalidParameterException:
                log.Printf("[ERROR] %v, %v", cidp.ErrCodeInvalidParameterException, aerr.Error())
            case cidp.ErrCodeUserNotFoundException:
                log.Printf("[ERROR] %v, %v", cidp.ErrCodeUserNotFoundException, aerr.Error())
            case cidp.ErrCodeUsernameExistsException:
                log.Printf("[ERROR] %v, %v", cidp.ErrCodeUsernameExistsException, aerr.Error())
            case cidp.ErrCodeInvalidPasswordException:
                log.Printf("[ERROR] %v, %v", cidp.ErrCodeInvalidPasswordException, aerr.Error())
            case cidp.ErrCodeCodeDeliveryFailureException:
                log.Printf("[ERROR] %v, %v", cidp.ErrCodeCodeDeliveryFailureException, aerr.Error())
            case cidp.ErrCodeUnexpectedLambdaException:
                log.Printf("[ERROR] %v, %v", cidp.ErrCodeUnexpectedLambdaException, aerr.Error())
            case cidp.ErrCodeUserLambdaValidationException:
                log.Printf("[ERROR] %v, %v", cidp.ErrCodeUserLambdaValidationException, aerr.Error())
            case cidp.ErrCodeInvalidLambdaResponseException:
                log.Printf("[ERROR] %v, %v", cidp.ErrCodeInvalidLambdaResponseException, aerr.Error())
            case cidp.ErrCodePreconditionNotMetException:
                log.Printf("[ERROR] %v, %v", cidp.ErrCodePreconditionNotMetException, aerr.Error())
            case cidp.ErrCodeInvalidSmsRoleAccessPolicyException:
                log.Printf("[ERROR] %v, %v", cidp.ErrCodeInvalidSmsRoleAccessPolicyException, aerr.Error())
            case cidp.ErrCodeInvalidSmsRoleTrustRelationshipException:
                log.Printf("[ERROR] %v, %v", cidp.ErrCodeInvalidSmsRoleTrustRelationshipException, aerr.Error())
            case cidp.ErrCodeTooManyRequestsException:
                log.Printf("[ERROR] %v, %v", cidp.ErrCodeTooManyRequestsException, aerr.Error())
            case cidp.ErrCodeNotAuthorizedException:
                log.Printf("[ERROR] %v, %v", cidp.ErrCodeNotAuthorizedException, aerr.Error())
            case cidp.ErrCodeUnsupportedUserStateException:
                log.Printf("[ERROR] %v, %v", cidp.ErrCodeUnsupportedUserStateException, aerr.Error())
            case cidp.ErrCodeInternalErrorException:
                log.Printf("[ERROR] %v, %v", cidp.ErrCodeInternalErrorException, aerr.Error())
            default:
                log.Printf("[ERROR] %v", err.Error())
            }
        } else {
            log.Printf("[ERROR] %v", err.Error())
        }
        return err
    }
    log.Printf("[INFO] Created new user %v successfully", e.EmailAddress)
    return nil
}

func (c *awsService) handler(e createUserEvent) (events.APIGatewayProxyResponse, error) {
    headers := map[string]string{"Content-Type": "application/json"}

    err := c.createUser(e)
    if err != nil {
        resp := lib.GenerateResponseBody(fmt.Sprintf("Error creating user %v", e.EmailAddress), 404, err, headers)
        return resp, nil
    }

    resp := lib.GenerateResponseBody(fmt.Sprintf("Created new user %v", e.EmailAddress), 200, nil, headers)
    return resp, nil
}

func main() {
    c := awsService{cidp.New(session.New())}

    lambda.Start(c.handler)
}

ma​​in_test.go

package main

import (
    "os"
    "testing"

    cidp "github.com/aws/aws-sdk-go/service/cognitoidentityprovider"
    cidpif "github.com/aws/aws-sdk-go/service/cognitoidentityprovider/cognitoidentityprovideriface"
)

type mockCreateUser struct {
    cidpif.CognitoIdentityProviderAPI
    Response cidp.AdminCreateUserOutput
}

func (d mockCreateUser) CreateUser(e createUserEvent) error {
    return nil
}

func TestCreateUser(t *testing.T) {
    t.Run("Successfully create user", func(t *testing.T) {
        m := mockCreateUser{Response: cidp.AdminCreateUserOutput{}}
        c := awsService{m}

        err := os.Setenv("USER_POOL_ID", "ap-southeast-2_ovum4dzAL")
        if err != nil {
            t.Fatal(err)
        }

        err = c.createUser(createUserEvent{EmailAddress: "user@example.com"})
        if err != nil {
            t.Fatal("User should have been created")
        }
    })
}

错误

Running tool: /usr/local/go/bin/go test -timeout 30s -run ^TestCreateUser$ github.com/sean/repo/src/create_user

--- FAIL: TestCreateUser (0.00s)
    --- FAIL: TestCreateUser/Successfully_create_user (0.00s)
panic: runtime error: invalid memory address or nil pointer dereference [recovered]
    panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x60 pc=0x137c2f2]

goroutine 6 [running]:
testing.tRunner.func1.1(0x13e00c0, 0x173fd70)
    /usr/local/go/src/testing/testing.go:1072 +0x30d
testing.tRunner.func1(0xc000001b00)
    /usr/local/go/src/testing/testing.go:1075 +0x41a
panic(0x13e00c0, 0x173fd70)
    /usr/local/go/src/runtime/panic.go:969 +0x1b9
github.com/sean/repo/src/create_user.(*mockCreateUser).AdminCreateUser(0xc00000ee40, 0xc00006a200, 0xc00001e39d, 0x18, 0xc0000b24b0)
    <autogenerated>:1 +0x32
github.com/sean/repo/src/create_user.(*awsService).createUser(0xc000030738, 0x145bbca, 0x10, 0x18, 0x0)
    /Users/sean/code/github/sean/repo/src/create_user/main.go:39 +0x2b7
github.com/sean/repo/src/create_user.TestCreateUser.func1(0xc000001b00)
    /Users/sean/code/github/sean/repo/src/create_user/main_test.go:30 +0x10c
testing.tRunner(0xc000001b00, 0x1476718)
    /usr/local/go/src/testing/testing.go:1123 +0xef
created by testing.(*T).Run
    /usr/local/go/src/testing/testing.go:1168 +0x2b3
FAIL    github.com/sean/repo/src/create_user    0.543s
FAIL

1 个答案:

答案 0 :(得分:1)

我认为这里的问题是,您想模拟 AdminCreateUser() 方法,但实际上模拟了 CreateUser() 方法。

因此,当您创建 mockCreateUser 结构体的新实例时,该实例“实现”了 cidpif.CognitoIdentityProviderAPI 接口,然后在其上调用 AdminCreateUser() 方法,它并未实现并且失败。

您的 main_test.go 中的相关代码应该是这样的:

type mockCreateUser struct {
    cidpif.CognitoIdentityProviderAPI
    Response cidp.AdminCreateUserOutput
}

func (d mockCreateUser) CreateUser(e createUserEvent) error {
    return nil
}

添加以下“虚拟”应该就足够了(并删除 CreateUser() 方法):

func (d mockCreateUser) AdminCreateUser(*cidp.AdminCreateUserInput) (*cidp.AdminCreateUserOutput, error) {
    return d.Response, nil
}

此外,我想提出一种稍微不同的方法来对您的 Lambda 进行单元测试。您的代码在可测试性方面已经相当不错了。但你可以做得更好。

我建议创建一个“应用程序”,其行为类似于您的 awsService 结构,但不实现任何 AWS 接口。相反,它包含一个 configuration 结构。此配置包含您从环境中读取的值(例如 USER_POOL_IDEMAIL)以及 AWS 服务的实例。

我们的想法是您的所有方法和函数都使用此配置,允许您在单元测试期间使用模拟 AWS 服务并在运行时使用“适当的”服务实例。

以下是您的 Lambda 的简化版本。显然,命名等取决于您。还有很多错误处理缺失等。

我认为最大的优点是,您可以非常轻松地通过 configapplication 修改方法/函数的输入。如果您想在每次测试和不同行为中使用不同的电子邮件等,只需更改配置即可。

ma​​in.go

package main

import (
    "os"

    "github.com/aws/aws-lambda-go/events"
    "github.com/aws/aws-lambda-go/lambda"
    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/session"
    "github.com/aws/aws-sdk-go/service/cognitoidentityprovider"
    "github.com/aws/aws-sdk-go/service/cognitoidentityprovider/cognitoidentityprovideriface"
)

type createUserEvent struct {
    EmailAddress string `json:"email_address"`
}

type configuration struct {
    poolId string
    idp    cognitoidentityprovideriface.CognitoIdentityProviderAPI
}

type application struct {
    config configuration
}

func (app *application) createUser(event createUserEvent) error {
    input := &cognitoidentityprovider.AdminCreateUserInput{
        UserPoolId:             aws.String(app.config.poolId),
        Username:               aws.String(event.EmailAddress),
        DesiredDeliveryMediums: aws.StringSlice([]string{"EMAIL"}),
        ForceAliasCreation:     aws.Bool(true),
        UserAttributes: []*cognitoidentityprovider.AttributeType{
            {
                Name:  aws.String("email"),
                Value: aws.String(event.EmailAddress),
            },
        },
    }

    _, err := app.config.idp.AdminCreateUser(input)
    if err != nil {
        return err
    }

    return nil
}

func (app *application) handler(event createUserEvent) (events.APIGatewayProxyResponse, error) {
    err := app.createUser(event)
    if err != nil {
        return events.APIGatewayProxyResponse{}, err
    }

    return events.APIGatewayProxyResponse{}, nil
}

func main() {
    config := configuration{
        poolId: os.Getenv("USER_POOL_ID"),
        idp:    cognitoidentityprovider.New(session.Must(session.NewSession())),
    }

    app := application{config: config}

    lambda.Start(app.handler)
}

ma​​in_test.go

package main

import (
    "testing"

    "github.com/aws/aws-sdk-go/service/cognitoidentityprovider"
    "github.com/aws/aws-sdk-go/service/cognitoidentityprovider/cognitoidentityprovideriface"
)

type mockAdminCreateUser struct {
    cognitoidentityprovideriface.CognitoIdentityProviderAPI
    Response *cognitoidentityprovider.AdminCreateUserOutput
    Error    error
}

func (d mockAdminCreateUser) AdminCreateUser(*cognitoidentityprovider.AdminCreateUserInput) (*cognitoidentityprovider.AdminCreateUserOutput, error) {
    return d.Response, d.Error
}

func TestCreateUser(t *testing.T) {
    t.Run("Successfully create user", func(t *testing.T) {
        idpMock := mockAdminCreateUser{
            Response: &cognitoidentityprovider.AdminCreateUserOutput{},
            Error:    nil,
        }

        app := application{config: configuration{
            poolId: "test",
            idp:    idpMock,
        }}

        err := app.createUser(createUserEvent{EmailAddress: "user@example.com"})
        if err != nil {
            t.Fatal("User should have been created")
        }
    })
}
相关问题