Stablo
Stablo

Reputation: 131

How to catch passed parameter and change it's value in Mocked method

I need to unit test really simple method that downloads file from Azure Blob storage. - DownloadFileAsync. Here is whole class.

public class BlobStorageService : IBlobStorageService
    {
        private readonly BlobServiceClient _blobStorageService;

        public BlobStorageService(IBlobStorageConnector blobStorageConnector)
        {
            var connector = blobStorageConnector ?? throw new ArgumentNullException(nameof(blobStorageConnector));

            _blobStorageService = connector.GetBlobStorageClient();
        }

        public async Task<Stream> DownloadFileAsync(string fileName, string containerName)
        {
            var container = _blobStorageService.GetBlobContainerClient(containerName);
            var blob = container.GetBlobClient(fileName);

            if (await blob.ExistsAsync())
            {
                using (var stream = new MemoryStream())
                {
                    await blob.DownloadToAsync(stream);

                    stream.Position = 0;
                    return stream;
                }
            }
            return Stream.Null;
        }
    }
}

The problem is that it requires quite a lot of mocking. I'm quite new to the idea of testing, so probably it's much better way to do that.

public class BlobStorageServiceTests
    {
        private string _containerName = "containerTest";
        private string _blobName = "blob";

        [Fact]
        public async Task BlobStorageService_Should_Return_File()
        {
            // Arrange
            Mock<IBlobStorageConnector> connectorMock = new Mock<IBlobStorageConnector>();
            Mock<BlobServiceClient> blobServiceClientMock = new Mock<BlobServiceClient>();
            Mock<BlobContainerClient> blobContainerClientMock = new Mock<BlobContainerClient>();
            Mock<BlobClient> blobClientMock = new Mock<BlobClient>();
            Mock<Response<bool>> responseMock = new Mock<Response<bool>>();

            //Preparing result stream
            string testString = "testString";
            byte[] bytes = Encoding.ASCII.GetBytes(testString);
            Stream testStream = new MemoryStream(bytes);
            testStream.Position = 0;
            

            responseMock.Setup(x => x.Value).Returns(true);
            // this doesn't work, passed stream is not changed, does callback work with value not reference?
            blobClientMock.Setup(x => x.DownloadToAsync(It.IsAny<Stream>(), CancellationToken.None)).Callback<Stream, CancellationToken>((stm, token) => stm = testStream);
            blobClientMock.Setup(x => x.ExistsAsync(CancellationToken.None)).ReturnsAsync(responseMock.Object);
            
            blobContainerClientMock.Setup(x => x.GetBlobClient(_blobName)).Returns(blobClientMock.Object);
            blobServiceClientMock.Setup(x => x.GetBlobContainerClient(_containerName)).Returns(blobContainerClientMock.Object);

            connectorMock.Setup(x => x.GetBlobStorageClient()).Returns(blobServiceClientMock.Object);

            BlobStorageService blobStorageService = new BlobStorageService(connectorMock.Object); ;
            
            // Act

            var result = await blobStorageService.DownloadFileAsync(_blobName, _containerName);

            StreamReader reader = new StreamReader(result);
            string stringResult = reader.ReadToEnd();

            // Assert

            stringResult.Should().Contain(testString);
        
        }
    }

Everything works like a charm and only small part of the test causes problem.

This part to be exact:


 // This callback works
            blobClientMock.Setup(x => x.ExistsAsync(CancellationToken.None)).ReturnsAsync(responseMock.Object).Callback(() => Trace.Write("inside job"));

// this doesn't work, does callback not fire?
            blobClientMock.Setup(x => x.DownloadToAsync(It.IsAny<Stream>(), CancellationToken.None)).ReturnsAsync(dynamicResponseMock.Object).Callback<Stream, CancellationToken>((stm, token) => Trace.Write("inside stream"));

//Part of tested class where callback should fire

if (await blob.ExistsAsync())
            {
                using (var stream = new MemoryStream())
                {
                    await blob.DownloadToAsync(stream);

                    stream.Position = 0;
                    return stream;
                }
            }

The last part has slightly different code as in the beggining, I'm trying to just write to Trace. "Inside Job" shows well, "Inside stream" not at all. Is the callback not being fired? What can be wrong here?

Upvotes: 2

Views: 1069

Answers (2)

Dina Bogdan
Dina Bogdan

Reputation: 4738

I know that it is not 100% the answer to this question, but you can solve the mocking problem by creating a stub like below:

public sealed class StubBlobClient : BlobClient
{
    private readonly Response _response;

    public StubBlobClient(Response response)
    {
        _response = response;
    }

    public override Task<Response> DownloadToAsync(Stream destination)
    {
        using (var archive = new ZipArchive(destination, ZipArchiveMode.Create, true))
        {
            var jsonFile = archive.CreateEntry("file.json");
            using var entryStream = jsonFile.Open();
            using var streamWriter = new StreamWriter(entryStream);
            streamWriter.WriteLine(TrunkHealthBlobConsumerTests.TrunkHealthJsonData);
        }
        return Task.FromResult(_response);
    }
}

and one more stub for BlobContainerClient like below:

public sealed class StubBlobContainerClient : BlobContainerClient
{
    private readonly BlobClient _blobClient;

    public StubBlobContainerClient(BlobClient blobClient)
    {
        _blobClient = blobClient;
    }

    public override AsyncPageable<BlobHierarchyItem> GetBlobsByHierarchyAsync(
        BlobTraits traits = BlobTraits.None,
        BlobStates states = BlobStates.None,
        string delimiter = default,
        string prefix = default,
        CancellationToken cancellationToken = default)
    {
        var item = BlobsModelFactory.BlobHierarchyItem("some prefix", BlobsModelFactory.BlobItem(name: "trunk-health-regional.json"));

        Page<BlobHierarchyItem> page = Page<BlobHierarchyItem>.FromValues(new[] { item }, null, null);

        var pages = new[] { page };

        return AsyncPageable<BlobHierarchyItem>.FromPages(pages);
    }

    public override BlobClient GetBlobClient(string prefix)
    {
        return _blobClient;
    }
}

then simply arrange your test like this:

var responseMock = new Mock<Response>();
var blobClientStub = new StubBlobClient(responseMock.Object);
var blobContainerClientStub = new StubBlobContainerClient(blobClientStub);

Upvotes: 0

Nkosi
Nkosi

Reputation: 247223

As mentioned in the comments, you need to write to the captured stream, not replace it, to get the expected behavior

//...

blobClientMock
    .Setup(x => x.DownloadToAsync(It.IsAny<Stream>(), CancellationToken.None))
    .Returns((Stream stm, CancellationToken token) => testStream.CopyToAsync(stm, token));

//...

Upvotes: 4

Related Questions