如何捕获传递的参数并在 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);
我需要对从 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);