【问题标题】:Mock CloudBlobClient with AutoFac and AutoMock用Autofac和Automock Mock CloudBlobClient
【发布时间】:2018-02-03 04:30:22
【问题描述】:

我正在尝试为我的 AzureBlobRepository 编写单元测试。存储库在构造函数中接收 CloubBlobClient。我想模拟客户端,但这给出了一个例外:

using (var mock = AutoMock.GetLoose())
{
    var mockClient = mock.Mock<CloudBlobClient>();
}

无法在类型“Microsoft.WindowsAzure.Storage.Blob.CloudBlobClient”上的多个长度为 2 的构造函数之间进行选择。注册组件时,使用 UsingConstructor() 配置方法显式选择构造函数。

当然,在我的单元测试中我没有注册任何东西,所以该消息不是很有帮助。

我还尝试了其他方法,例如提供 NameParameters、TypedParameters 或调用 mock.Create 代替 mock.Mock,但我尝试的所有方法都返回相同的异常消息。

(同样的问题也出现在 CloudBlobContainer 上)

实现接口后更新 这是我编写的单元测试示例:

[TestMethod]
public void AzureBlobRepository_GetByIdAsync_ReturnsContent()
{    
    Guid blobId = Guid.NewGuid();
    Guid directoryName = Guid.NewGuid();
    string containerName = "unittest";

    using (var mock = AutoMock.GetLoose())
    {
        var mockClient = mock.Mock<ICloudBlobClient>();
        var mockContainer = mock.Mock<ICloudBlobContainer>();
        var mockDirectory = mock.Mock<ICloudBlobDirectory>();
        // notice that we're not using AutoMock here, it fails to create the mock
        var mockBlob = new Mock<CloudBlockBlob>(new Uri($"http://tempuri.org/{containerName}/{directoryName}/{blobId}"));
        mockBlob.Setup(m => m.DownloadTextAsync()).Returns(Task.FromResult("content"));

        mockClient.Setup(m => m.GetContainerReference(containerName))
            .Returns(mockContainer.Object);
        mockContainer.Setup(m => m.GetDirectoryReference(directoryName.ToString()))
            .Returns(mockDirectory.Object);
        mockDirectory.Setup(m => m.GetBlockBlobReference(blobId.ToString()))
            .Returns(mockBlob.Object);

        var repository = mock.Create<AzureBlobRepository>(
            new TypedParameter(typeof(ICloudBlobClient), mockClient.Object),
            new NamedParameter("container", containerName),
            new NamedParameter("directory", directoryName));

        var result = repository.GetByIdAsync(blobId, directoryName).Result;
        result.ShouldBe("content");
    }
}

【问题讨论】:

  • 这些类应被视为第 3 方实施问题。这意味着您无法控制它们,我们不应该嘲笑我们无法控制的东西。它们应该封装在您可以控制的抽象后面,并且可以在单独测试时根据需要进行模拟。由于这个原因,类应该依赖于抽象而不是具体。模拟具体类往往会产生连锁反应
  • 这些类已在 Azure 存储 SDK 的最新更新中进行了修改(虚拟化方法),用于单元测试。
  • 好吧,我不知道。文档尚未更新以反映该更改。
  • 我相信Provide可以工作。在这种情况下,它只是被错误地使用了。看docs.autofac.org/en/latest/integration/…
  • @Nkosi 也许你对添加抽象层的看法是正确的。在我的情况下,我可以随意设置 CloudBlobClient 和 CloudBlobContainer,但是我也必须使用 CloudBlobDirectory,它没有公共构造函数,因此不能由 Moq 创建..

标签: c# unit-testing moq autofac azure-blob-storage


【解决方案1】:

这些类应被视为第 3 方实施问题。这意味着您无法控制它们,我们不应该嘲笑我们无法控制的东西。它们应该封装在您可以控制的抽象后面,并且可以在单独测试时根据需要进行模拟。

public interface ICloudBlobClient {
    //...expose only the functionality I need
}

public class CloudBlobClientWrapper : ICloudBlobClient {
    private readonly CloudBlobClient client;

    public CloudBlobClientWrapper(CloudBlobClient client) {
        this.client = client;
    }

    //...implement interface wrapping
}

正是因为这个原因,类应该依赖于抽象而不是具体化。模拟具体类往往会产生连锁反应

包装器不需要完全包装客户端,但可以聚合功能,以免暴露实现问题。

所以现在在隔离测试时,您可以模拟您控制的抽象

using (var mock = AutoMock.GetLoose()) {
    var mockClient = mock.Mock<ICloudBlobClient>();

    /// ...and the rest of the test.
}

【讨论】:

  • 是的,这种方法有效。我最终得到了 CloudBlobClient、CloudBlobContainer 和 CloudBlobDirectory 的接口以及这些接口的包装器。所以相当多的额外工作,但至少我现在可以继续编写单元测试!我用示例单元测试更新了原始问题
【解决方案2】:

看到这个CloudBlobContainer。此类型包含三个构造函数。创建类型的实例需要构造函数。尝试输入您的代码new CloudBlobContainer,您将需要选择三个构造函数之一。 AutoMock 无法知道构造函数必须选择什么。

你可以告诉 AutoMock 如何创建 CloudBlobContainer

示例:

using (var mock = AutoMock.GetLoose())
{
    mock.Provide<CloudBlobContainer, CloudBlobContainer>(new NamedParameter("uri", new Uri("your uri")));
    var mockClient = mock.Mock<CloudBlobClient>();
}

【讨论】:

  • 太糟糕了,mock.Provide 抛出了完全相同的异常
  • 是的,这比我预期的要隐含和困难。它看起来像 AutoMock 中的缺陷。需要阅读 AutoMock 的源码
  • @Hanno 我相信提供可以工作。在这种情况下,它只是被错误地使用了。看docs.autofac.org/en/latest/integration/…
  • 我想我必须同意@SergeyRubtsov 的观点,即这是 AutoMock 的一个缺点。直接使用 Moq 在模拟 CloudBlobClient 和 CloudBlobContainer 时完全没有问题。
【解决方案3】:

我已经设法使用NSubstitute 模拟了它,我只模拟了我使用的函数。

/// <summary>
/// Create a mock for CloudBlobClient
/// </summary>
/// <param name="containerExists"></param>
/// <returns></returns>
private CloudBlobClient GetMock(bool containerExists = true)
{
    var items = new List<IListBlobItem>();
    var client = Substitute.For<CloudBlobClient>(new Uri("http://foo.bar/"), null);
    var container = Substitute.For<CloudBlobContainer>(new Uri("http://foo.bar/"));
    client.GetContainerReference(Arg.Any<string>()).Returns(container);
    container.ExistsAsync(Arg.Any<CancellationToken>()).Returns(Task.FromResult(containerExists));
    container.ListBlobsSegmentedAsync(Arg.Any<string>(), Arg.Any<bool>(), 
                                        Arg.Any<BlobListingDetails>(), 
                                        Arg.Any<int?>(), 
                                        Arg.Any<BlobContinuationToken>(), 
                                        Arg.Any<BlobRequestOptions>(), 
                                        Arg.Any<OperationContext>(), 
                                        Arg.Any<CancellationToken>())
                                        .Returns(ci => new BlobResultSegment(items.ToArray(), null));

    container.GetBlockBlobReference(Arg.Any<string>()).Returns(ci => GetBlockBlobMock(ci.ArgAt<string>(0), items));
    return client;
}

/// <summary>
/// Create a mock for CloudBlockBlob
/// </summary>
/// <param name="name"></param>
/// <param name="listBlobItems"></param>
/// <returns></returns>
private CloudBlockBlob GetBlockBlobMock(string name, List<IListBlobItem> listBlobItems)
{
    var created = DateTimeOffset.Now;
    var bufferStream = new MemoryStream();
    var blob = Substitute.For<CloudBlockBlob>(new Uri("https://foo.blob.core.windows.net/bar/" + name + ".txt"));
    //We can't mock the value the normal way, use reflection to change its value!
    blob.Properties.GetType().GetProperty(nameof(blob.Properties.Created)).SetValue(blob.Properties, created);
    //we cant mock properties! (Dam this wont work)
    blob.UploadFromStreamAsync(Arg.Any<Stream>(),
                                Arg.Any<AccessCondition>(),
                                Arg.Any<BlobRequestOptions>(),
                                Arg.Any<OperationContext>(),
                                Arg.Any<CancellationToken>()).Returns(ci => {
                                    var stream = ci.Arg<Stream>();
                                    stream.CopyTo(bufferStream);
                                    listBlobItems.Add(blob);
                                    return Task.CompletedTask;
                                });

    blob.DownloadToStreamAsync(Arg.Any<Stream>(),
                                Arg.Any<AccessCondition>(),
                                Arg.Any<BlobRequestOptions>(),
                                Arg.Any<OperationContext>(),
                                Arg.Any<CancellationToken>()).Returns(ci =>
                                {
                                    var stream = ci.Arg<Stream>();
                                    bufferStream.Position = 0;
                                    bufferStream.CopyTo(stream);
                                    stream.Position = 0;
                                    return Task.CompletedTask;
                                });
    return blob;
}

我不能 100% 确定它 100% 准确,但它允许我运行一些单元测试!

【讨论】:

    猜你喜欢
    • 2012-04-13
    • 2021-01-09
    • 2019-08-29
    • 2021-10-10
    • 2016-10-30
    • 1970-01-01
    • 1970-01-01
    • 2015-04-04
    • 2011-10-18
    相关资源
    最近更新 更多