有没有办法模拟 Azure CloudQueueClient 或 CloudQueue?

9

我正在为我的代码编写单元测试,遇到了一个方法,在创建队列并将消息添加到队列时抛出StorageException异常。 我想测试异常处理是否正常。 为此,我有一个使用CloudQueue模拟的想法,但后来发现该类是密封的。 在不实际更改生产代码的情况下,有没有办法测试异常处理(或强制引发StorageException)?


1
我看到的一种方法是,您需要在接口中包装CloudQueue,然后模拟此接口以抛出您想要处理的所需异常。但这将需要更改您的代码。我没有看到其他方法。 - tyrion
是的,我考虑过这种方法。然而,我不想仅为了测试而重构我的代码。所以,我猜没有办法同时覆盖这两种结果,对吧? - SalysBruoga
3个回答

9

处理这个问题最简单的方法是使用一个围绕着CloudQueueClient的接口(就像上面@tyrion所建议的那样)...但同时也需要一个ICloudQueue的接口

public interface ICloudQueueClientWrapper
{
    ICloudQueueWrapper GetQueueReference(string queueName);
}

// ----------------

public class CloudQueueClientWrapper : ICloudQueueClientWrapper
{
    private readonly Lazy<CloudQueueClient> _cloudQueueClient;

    public CloudQueueClientWrapper(string connectionStringName)
    {
        connectionStringName.ShouldNotBeNullOrWhiteSpace();

        _cloudQueueClient = new Lazy<CloudQueueClient>(() =>
        {
            // We first need to connect to our Azure storage.
            var storageConnectionString = CloudConfigurationManager.GetSetting(connectionStringName);
            var storageAccount = CloudStorageAccount.Parse(storageConnectionString);

            // Create the queue client.
            return storageAccount.CreateCloudQueueClient();
        });
    }

    public ICloudQueueWrapper GetQueueReference(string queueName)
    {
        queueName.ShouldNotBeNullOrWhiteSpace();

        var cloudQueue = _cloudQueueClient.Value.GetQueueReference(queueName);
        return new CloudQueueWrapper(cloudQueue);
    }

    // Add more methods here which are a one-to-one match against the underlying CQC.
}

这就是第一个接口和包装器...注意,它返回一个ICloudQueue 实例..所以现在让我们来做吧...

public interface ICloudQueueWrapper
{
    Task AddMessageAsync(CloudQueueMessage message);
}

public class CloudQueueWrapper : ICloudQueueWrapper
{
    private readonly CloudQueue _cloudQueue;

    public CloudQueueWrapper(CloudQueue cloudQueue)
    {
        cloudQueue.ShouldNotBeNull();

        _cloudQueue = cloudQueue;
    }

    public async Task AddMessageAsync(CloudQueueMessage message)
    {
        message.ShouldNotBeNull();

        await _cloudQueue.AddMessageAsync(message);
    }
}

好的,现在让我们尝试在一些单元测试中使用它 :)

    [Theory]
    [MemberData(nameof(StockIds))]
    public async Task GivenSomeData_DoFooAsync_AddsDataToTheQueue(string[] stockIds)
    {
        // Arrange.
        var cloudQueue = Mock.Of<ICloudQueueWrapper>();
        var cloudQueueClient = Mock.Of<ICloudQueueClientWrapper>();
        Mock.Get(cloudQueueClient).Setup(x => x.GetQueueReference(It.IsAny<string>()))
            .Returns(cloudQueue);
        var someService = new SomeService(cloudQueueClient);

        // Act.
        await someService.DoFooAsync(Session);

        // Assert.
        // Did we end up getting a reference to the queue?
        Mock.Get(cloudQueueClient).Verify(x => x.GetQueueReference(It.IsAny<string>()), Times.Once);

        // Did we end up adding something to the queue?
        Mock.Get(cloudQueue).Verify(x => x.AddMessageAsync(It.IsAny<CloudQueueMessage>()), Times.Exactly(stockids.Length));
    }

1

谢谢,我会研究一下。 - SalysBruoga

0

@Pure.Krome 提供的解决方案是一个不错的方法 - 我只想指出他对 CloudQueueClientWraper 实现可能存在的一个问题:

public class CloudQueueClientWrapper : ICloudQueueClientWrapper
{
    private readonly Lazy<CloudQueueClient> _cloudQueueClient;

    public CloudQueueClientWrapper(string connectionStringName)
    {
        connectionStringName.ShouldNotBeNullOrWhiteSpace();

        _cloudQueueClient = new Lazy<CloudQueueClient>(() =>
        {
            // We first need to connect to our Azure storage.
            var storageConnectionString = CloudConfigurationManager.GetSetting(connectionStringName);
            var storageAccount = CloudStorageAccount.Parse(storageConnectionString);

            // Create the queue client.
            return storageAccount.CreateCloudQueueClient();
        });
    }

    public ICloudQueueWrapper GetQueueReference(string queueName)
    {
        queueName.ShouldNotBeNullOrWhiteSpace();

        var cloudQueue = _cloudQueueClient.Value.GetQueueReference(queueName);
        return new CloudQueueWrapper(cloudQueue);
    }

    // Add more methods here which are a one-to-one match against the underlying CQC.
}

Lazy<T>缓存异常!因此,如果一个线程(或者更确切地说是Task)尝试通过执行valueFactory委托来创建.Value失败并抛出异常,则所有后续对.Value的调用都将返回相同的异常。

docs中提到:

异常缓存 当您使用工厂方法时,异常会被缓存。也就是说,如果工厂方法在第一次尝试访问Lazy对象的Value属性时抛出异常,则每个后续尝试都会抛出相同的异常。这确保了对Value属性的每个调用都产生相同的结果,并避免了可能由于不同的线程获得不同结果而引起的微妙错误。Lazy代替了实际上本应在某个较早时间点初始化的T。在那个较早时间点的失败通常是致命的。如果存在可恢复故障的潜在风险,我们建议您将重试逻辑构建到初始化例程中(在这种情况下,即工厂方法),就像您没有使用延迟初始化一样。

作为一种“解决方法”,放弃内置的Lazy<T>并实现自己的版本。 @mariusGundersen在this GitHub问题中提出了一种优雅的实现方式。
public class AtomicLazy<T>
{
    private readonly Func<T> _factory;

    private T _value;

    private bool _initialized;

    private object _lock;

    public AtomicLazy(Func<T> factory)
    {
        _factory = factory;
    }

    public AtomicLazy(T value)
    {
        _value = value;
        _initialized = true;
    }

    public T Value => LazyInitializer.EnsureInitialized(ref _value, ref _initialized, ref _lock, _factory);
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接