如何捕获传递的参数并在 Mocked 方法中更改它的值

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

我需要对从 Azure Blob 存储下载文件的非常简单的方法进行单元测试。 - 下载文件异步。这是整个 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;
        }
    }
}

问题是它需要大量的模拟。我对测试的想法很陌生,所以这可能是更好的方法。

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

一切都很顺利,只有一小部分测试会导致问题。

这部分要准确:


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

最后一部分与开头部分的代码略有不同,我试图只写给 Trace。 “Inside Job”表现得很好,“Inside stream”则不然。回调没有被触发吗?这里有什么问题吗?

如评论中所述,您需要写入捕获的流,而不是替换它,以获得预期的行为

//...

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

//...

我知道这不是这个问题的 100% 答案,但您可以通过创建如下所示的存根来解决模拟问题:

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

还有一个 BlobContainerClient 的存根,如下所示:

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

然后像这样简单地安排你的测试:

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