web-dev-qa-db-ja.com

AWSLambda環境変数と依存性注入

AWSLambdaを.NETCore v1.0で使用するときに、依存性注入または環境変数のモックを使用するために利用できるベストプラクティスまたはドキュメントはありますか?

例として、以下は、KinesisEventを受け入れ、ある種の処理を行うLambda関数ProcessKinesisMessageByIdの例です。この処理の一部には、セットアップのために環境変数へのアクセスを必要とするある種の外部サービス(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関数をローカルでテストするための提案や実践はありますか?

9
apleroy

これがAWSLambda関数であるという事実は実装上の懸念事項であり、現在の状態のコードを単独でテストすることが難しいという事実とはあまり関係がないはずです。これは設計上の問題です。

コードをリファクタリングして、もう少し柔軟で保守しやすくすることを検討してください。

環境変数に関しては、抽象化の背後にある静的クラスをカプセル化して、疎結合とより良いモックを可能にすることを検討してください。

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;
    }
}

上記の2つの提案により、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;
    });
}

レイジーファクトリは、テスト時にモックできる抽象化を公開するため、テスト時に必要に応じて置き換えることができます。

次の例では、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());
}

実際、この設計では、システム環境の抽象化も、どこでどのように使用されているかに基づいて実装上の懸念事項と見なすことができるため、完全に削除できます。

11
Nkosi

この回答は、@ Nkosiの回答からの推奨事項を実装する試みです。

私はレイジーファクトリをオーバーライドする方法に精通しておらず、さまざまな方法を試しました。以下は、これを実現するための実装方法の試みです。環境変数の新しい抽象化は、レイジーファクトリによって作成された依存関係を受け入れるためのILambdaContextインターフェイスの新しい実装とともに以下に含まれています。元の質問を補強し、短いコメントを超えて@Nkosiの非常に役立つ回答に拡張するために、この回答を投稿します。

//コード開始

これはAWSLambda関数です-リクエストのみを受け入れ、新しく作成されたサービス(処理ロジックが存在する場所)に渡すようにリファクタリングされています

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);
        }
    }
3
apleroy