Search code examples
c#unit-testingamazon-s3moqawss3transferutility

how to mock/unit test downloads from amazon s3?


I am new to unit testing/moq and was wondering how this could be done. I have a function that downloads a file from s3 to local. How can I mock this so that it doesn't actually use transferUtility to download anything from s3?

bool downloadFileFromS3(string localPathToSave, string filenameToDownloadAs, string bucketName, string objectKey)
{
    try
    {
        string accessKey = "xxx";
        string secretKey = "xxx";
        // do stuff before
        TransferUtility transferUtility = new TransferUtility(accessKey, secretKey);
        transferUtility.Download( Path.Combine(localPathToSave, filenameToDownloadAs), bucketName, objectKey );
        // do stuff after
        return true;
    }

    catch (Exception e)
    {
        // stuff
    }
}

I've created the mock but I don't know how to use it for testing the function that I wrote.

[Test]
public void testDownloadFromS3()
{
    string filePath = null;
    string bucketName = null;
    string key = null;

    Mock<Amazon.S3.Transfer.TransferUtility> mock = new Mock<Amazon.S3.Transfer.TransferUtility>();
    //mock.Setup(x => x.Download(filePath, bucketName, key)).Verifiable();
    //Mock.Verify();
}

Solution

  • According to the documentation the TransferUtility class implements the ITransferUtility interface.

    If you need to test your downloadFileFromS3 then your code should depend on abstraction, not on concretions as the SOLID's DIP says.

    private readonly ITransferUtility transferUtility; //should be set via constructor injection
    
    bool downloadFileFromS3(string localPathToSave, string filenameToDownloadAs, string bucketName, string objectKey)
    {
        try
        {
            // do stuff before
    
            transferUtility.Download( Path.Combine(localPathToSave, filenameToDownloadAs), bucketName, objectKey );
    
            // do stuff after
            return true;
        }
    
        catch (Exception e)
        {
            // stuff
        }
    }
    

    Because the Download method does not return anything all you need to do is to setup verifyability in case of happy path

    [Test]
    public void testDownloadFromS3_HappyPath()
    {
        //Arrange
        const string localPathToSave = "C:/tmp/";
        const string filenameToDownloadAs = "test.txt";
        const string bucketName = "x";
        const string objectKey = "y";
    
        Mock<Amazon.S3.Transfer.ITransferUtility> transferUtilMock = new Mock<Amazon.S3.Transfer.ITransferUtility>();
        transferUtilMock
            .Setup(util => util.Download(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))
            .Verifiable();
    
        var sut = new ClassThatContainsTheDownloadFileFromS3(transferUtilMock.Object);
    
        //Act
        var result = sut.downloadFileFromS3(localPathToSave, filenameToDownloadAs, bucketName, objectKey);
    
        //Assert
        Assert.IsTrue(result);
        transferUtilMock.Verify(util => util.Download(
                It.Is<string>(filePath => string.Equals(filePath, localPathToSave +filenameToDownloadAs, StringComparison.InvariantCultureIgnoreCase)),
                It.Is<string>(bucket => string.Equals(bucket, bucketName, StringComparison.InvariantCultureIgnoreCase)),
                It.Is<string>(key => string.Equals(key, objectKey, StringComparison.InvariantCultureIgnoreCase)),
            Times.Once);
    }
    
    • We have set up the mock to accept any parameters (It.IsAny)
    • We have passed the mock instance's Object property to the class which contains the downloadFileFromS3
    • I've used a name sut, which refers to System Under Test
    • We have used It.Is to verify that the Download method has been called in the way we expect
    • We also pass a Times.Once to the Verify to make sure that the Download has been called exactly once.

    This is how you can test the unhappy path:

    [Test]
    public void testDownloadFromS3_UnhappyPath()
    {
        //Arrange
        const string localPathToSave = "C:/tmp/";
        const string filenameToDownloadAs = "test.txt";
        const string bucketName = "x";
        const string objectKey = "y";
    
        Mock<Amazon.S3.Transfer.ITransferUtility> transferUtilMock = new Mock<Amazon.S3.Transfer.ITransferUtility>();
        transferUtilMock
            .Setup(util => util.Download(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))
            .Throws<System.IO.IOException>();;
    
        var sut = new ClassThatContainsTheDownloadFileFromS3(transferUtilMock.Object);
    
        //Act
        var result = sut.downloadFileFromS3(localPathToSave, filenameToDownloadAs, bucketName, objectKey);
    
        //Assert
        Assert.IsFalse(result);
        transferUtilMock.Verify(util => util.Download(
                It.Is<string>(filePath => string.Equals(filePath, localPathToSave +filenameToDownloadAs, StringComparison.InvariantCultureIgnoreCase)),
                It.Is<string>(bucket => string.Equals(bucket, bucketName, StringComparison.InvariantCultureIgnoreCase)),
                It.Is<string>(key => string.Equals(key, objectKey, StringComparison.InvariantCultureIgnoreCase)),
            Times.Once);
    }
    
    • As you can see we have replaced the Verifiable call to Throws to throw exception whenever it is called
    • I've also replaced the Assert.IsTrue to Assert.IsFalse
      • I've assumed that in case of exception // stuff returns false