AWS Lambda环境变量和依赖注入

时间:2017-10-27 23:55:56

标签: c# dependency-injection environment-variables .net-core aws-lambda

在将AWS Lambda与.NET Core v1.0一起使用时,是否有可用于使用依赖注入或模拟环境变量的最佳实践或文档?

作为示例,下面是一个示例Lambda函数ProcessKinesisMessageById,它接受​​KinesisEvent并进行某种处理。此处理的一部分涉及访问需要访问环境变量以进行设置的某种外部服务(如AWS S3或数据库)。

public class AWSLambdaFileProcessingService
{
    private IFileUploadService _fileUploadService;

    // No constructor in the Lambda Function

    [LambdaSerializer(typeof(JsonSerializer))]
    public void ProcessKinesisMessageById(KinesisEvent kinesisEvent, ILambdaContext context)
    {
        Console.WriteLine("Processing Kinesis Request");

        _fileUploadService = new AWSFileUploadService(); // Can this be injected? (Constructor shown below)

        // some sort of processing
        _fileUploadService.DoSomethingWithKinesisEvent(kinesisEvent);
    }
}

// Example of of a class that needs access to environment variables
// Can this class be injected into the AWS Lambda function?  
// Or the Environment Variables mocked?
public class AWSFileUploadService : IFileUploadService
{
    private readonly IAmazonS3 _amazonS3Client;
    private readonly TransferUtility _fileTransferUtility;


    public AWSFileUploadService()
    {
        _amazonS3Client = new AmazonS3Client(
            System.Environment.GetEnvironmentVariable("AWS_S3_KEY"),
            System.Environment.GetEnvironmentVariable("AWS_S3_SECRET_KEY")
            );

        _fileTransferUtility = new TransferUtility(_amazonS3Client);
    }

    public bool DoSomethingWithKinesisEvent(KinesisEvent kinesisEvent)
    {
        // ....
    }

```

使用环境变量发布后,该功能可以正常工作,并且可以在将其发布到AWS后使用Lambda Function View测试控制台(在Visual Studio 2017中)进行测试。但是,我无法模拟或设置环境变量以用于本地测试,因此无法创建单元测试或集成测试。

有没有人有任何关于在本地测试Lambda函数的建议或做法?

2 个答案:

答案 0 :(得分:11)

这是AWS Lambda函数的事实是实现问题,并且实际上不应该对当前状态的代码难以单独测试这一事实有很大的影响。这是设计问题。

考虑重构代码以使其更灵活/可维护。

关于环境变量,考虑将静态类封装在抽象后面,以允许更松散的耦合和更好的模拟。

public interface ISystemEnvironment {
    string GetEnvironmentVariable(string variable);
}

public class SystemEnvironmentService : ISystemEnvironment {
    public string GetEnvironmentVariable(string variable) {
        return System.Environment.GetEnvironmentVariable(variable);
    }
}

基于提供的示例,AWSFileUploadService与实现问题紧密结合,存在可以利用的抽象。

public class AWSFileUploadService : IFileUploadService {
    private readonly IAmazonS3 _amazonS3Client;
    private readonly TransferUtility _fileTransferUtility;

    public AWSFileUploadService(IAmazonS3 s3) {
        _amazonS3Client = s3;
        //Not sure about this next class but should consider abstracting it as well.
        _fileTransferUtility = new TransferUtility(_amazonS3Client);
    }

    public bool DoSomethingWithKinesisEvent(KinesisEvent kinesisEvent) {
        //code removed for brevity
        return true;
    }
}

根据上述两条建议,AWSLambdaFileProcessingService现在可以重构为

public class AWSLambdaFileProcessingService {
    private IFileUploadService _fileUploadService;

    [LambdaSerializer(typeof(JsonSerializer))]
    public void ProcessKinesisMessageById(KinesisEvent kinesisEvent, ILambdaContext context) {
        Console.WriteLine("Processing Kinesis Request");
        _fileUploadService = FileUploadService.Value;
        // some sort of processing
        _fileUploadService.DoSomethingWithKinesisEvent(kinesisEvent);
    }

    public static Lazy<IFileUploadService> FileUploadService = new Lazy<IFileUploadService>(() => {
        var env = new SystemEnvironmentService();
        var s3 = new AmazonS3Client(
            env.GetEnvironmentVariable("AWS_S3_KEY"),
            env.GetEnvironmentVariable("AWS_S3_SECRET_KEY")
        );
        var service = new AWSFileUploadService(s3);
        return service;
    });
}

在测试时可以根据需要替换Lazy工厂,因为它暴露了一个可以在测试时被模拟的抽象。

以下示例使用Moq

[TestMethod]
public void TestKinesisMessage() {
    //Arrange
    var testMessage = "59d6572f028c52057caf13ff";
    var testStream = "testStream";
    var kinesisEvent = BuildKinesisTestRequest(testMessage, testStream);
    var lambdaServiceMock = new Mock<ILambdaContext>();
    var fileUploadServiceMock = new Mock<IFileUploadService>();            
    //Replace the  lazy initialization of the service
    AWSLambdaFileProcessingService.FileUploadService = 
        new Lazy<IFileUploadService>(() => fileUploadServiceMock.Object);
    var subject = new AWSLambdaFileProcessingService();

    //Act
    subject.ProcessKinesisMessageById(kinesisEvent, lambdaServiceMock.Object);

    //Assert
    fileUploadServiceMock.Verify(_ => _.DoSomethingWithKinesisEvent(kinesisEvent), Times.AtLeastOnce());
}

事实上,通过这种设计,系统环境抽象可以完全删除,因为它也可以被视为基于其使用位置和方式的实现问题。

答案 1 :(得分:3)

这个答案是为了实现@Nkosi回答的建议。

我不熟悉如何覆盖Lazy工厂并尝试不同的方法,以下是我尝试实现此方法的实现方法。下面包含了环境变量的新抽象以及ILambdaContext接口的新实现,以接受由惰性工厂创建的依赖项。我发布这个答案是为了增加原始问题,并将其扩展到@ Nkosi非常有用的答案之外。

//代码开始

这是AWS Lambda函数 - 重构为仅接受请求并传递到新创建的服务(处理逻辑所在的位置)

public class AWSLambdaFileProcessingService
{
    [LambdaSerializer(typeof(JsonSerializer))]
    public void ProcessKinesisMessageById(KinesisEvent kinesisEvent, ILambdaContext context)
    {
        Console.WriteLine("Processing Kinesis Request");

        IKinesisEventProcessingService kinesisEventProcessingService = new KinesisEventProcessingService(context);
        kinesisEventProcessingService.ProcessKinesisEvent(kinesisEvent);
    }
}

这是一项新服务,用于封装所有作用于输入的服务

public class KinesisEventProcessingService : IKinesisEventProcessingService
{
    private IFileUploadService _fileUploadService;

    // constructor to attach Lazy loaded IFileUploadService
    public KinesisEventProcessingService(ILambdaContext context)
    {
        AWSLambdaFileProcessingServiceContext AWSLambdaFileProcessingServiceContext =
            LambdaContextFactory.BuildLambdaContext(context);

        _fileUploadService = AWSLambdaFileProcessingServiceContext.FileUploadService;
    }

    public void ProcessKinesisEvent(KinesisEvent kinesisEvent)
    {

        _fileUploadService.DoSomethingWithKinesisEvent(kinesisEvent);
        // ....

    }
}

这是ILambdaContext的一个实现,也可用于测试此上下文,允许在测试中覆盖附加服务

public class AWSLambdaFileProcessingServiceContext : ILambdaContext
{
    public AWSLambdaFileProcessingServiceContext()
    {
        FileUploadService = default(IFileUploadService);
    }

    public string AwsRequestId { get; }
    // ... ILambdaContext properties
    public TimeSpan RemainingTime { get; }

    // Dependencies
    public IFileUploadService FileUploadService { get; set; }

}

// static class for attaching dependencies to the context
public static class LambdaContextFactory
{
    public static AWSLambdaFileProcessingServiceContext BuildLambdaContext(ILambdaContext context)
    {
        // cast to implementation that has dependencies as properties of context
        AWSLambdaFileProcessingServiceContext serviceContext = default(AWSLambdaFileProcessingServiceContext);

        if (context.GetType().Equals(typeof(AWSLambdaFileProcessingServiceContext)))
        {
            serviceContext = (AWSLambdaFileProcessingServiceContext)context;
        }
        else
        {
            serviceContext = new AWSLambdaFileProcessingServiceContext();
        }

        // lazily inject dependencies
        if (serviceContext.FileUploadService == null)
        {
            serviceContext.FileUploadService = FileUploadService.Value;
        }

        return serviceContext;
    }

    public static Lazy<IFileUploadService> FileUploadService = new Lazy<IFileUploadService>(() =>
    {
        ISystemEnvironmentService env = new SystemEnvironmentService();
        IAmazonS3 s3 = new AmazonS3Client(
            env.GetEnvironmentVariable("AWS_S3_KEY"),
            env.GetEnvironmentVariable("AWS_S3_SECRET_KEY")
        );
        IFileUploadService service = new AWSFileUploadService(s3);
        return service;
    });

这是Lambda函数测试的一个例子

    /// <summary>
    /// This tests asserts that the Lambda function handles the input and calls the mocked service
    /// </summary>
    [Fact()]
    public void TestKinesisMessage()
    {
        // arrange
        string testMessage = "59d6572f028c52057caf13ff";
        string testStream = "testStream";

        IFileUploadService FileUploadService = new AWSFileUploadService(new Mock<IAmazonS3>().Object);
        // create the custom context and attach above mocked FileUploadService from Lazy factory
        var context = new AWSLambdaFileProcessingServiceContext();
        context.FileUploadService = FileUploadService;

        var lambdaFunction = new AWSLambdaFileProcessingService();

        KinesisEvent kinesisEvent = BuildKinesisTestRequest(testMessage, testStream);

        // act & assert
        try
        {
            lambdaFunction.ProcessKinesisMessageById(kinesisEvent, context);
        }
        catch (Exception e)
        {
            // https://stackoverflow.com/questions/14631923/xunit-net-cannot-find-assert-fail-and-assert-pass-or-equivalent
            Assert.True(false, "Error processing Kinesis Message :" + e.StackTrace);
        }
    }