AWS Lambda环境变量和dependency injection

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

作为一个例子,下面是一个示例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) { // .... } 

“`

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

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

这是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 FileUploadService = new Lazy(() => { 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(); var fileUploadServiceMock = new Mock(); //Replace the lazy initialization of the service AWSLambdaFileProcessingService.FileUploadService = new Lazy(() => fileUploadServiceMock.Object); var subject = new AWSLambdaFileProcessingService(); //Act subject.ProcessKinesisMessageById(kinesisEvent, lambdaServiceMock.Object); //Assert fileUploadServiceMock.Verify(_ => _.DoSomethingWithKinesisEvent(kinesisEvent), Times.AtLeastOnce()); } 

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

这个答案试图实现@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 FileUploadService = new Lazy(() => { 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函数测试的一个例子

  ///  /// This tests asserts that the Lambda function handles the input and calls the mocked service ///  [Fact()] public void TestKinesisMessage() { // arrange string testMessage = "59d6572f028c52057caf13ff"; string testStream = "testStream"; IFileUploadService FileUploadService = new AWSFileUploadService(new Mock().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); } }