有没有办法模拟 Azure CloudQueueClient 或 CloudQueue?
Is there any way to mock Azure CloudQueueClient or CloudQueue?
我正在为我的代码编写单元测试并且遇到了一个方法,该方法在尝试创建队列并将 AddMessage 添加到队列时抛出 StorageException。我想测试异常处理是否正常。为了做到这一点,我想到了为 CloudQueue 使用模拟,但后来发现这个 class 是密封的。有没有什么方法可以在不实际更改生产代码的情况下测试异常处理(或强制 StorageException)?
我们过去使用 Microsoft Fakes 框架能够对类似的 Azure SDK 类 进行单元测试。前面有一点学习曲线,但效果很好。
处理这个问题的最简单方法是使用围绕 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.
}
这就是第一个接口和包装器...请注意这个 returns 一个 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));
}
@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
的所有后续调用将 return 相同的异常。
docs 说:
Exception caching When you use factory methods, exceptions are cached. That is, if the factory method throws an exception the first time a thread tries to access the Value property of the Lazy object, the same exception is thrown on every subsequent attempt. This ensures that every call to the Value property produces the same result and avoids subtle errors that might arise if different threads get different results. The Lazy stands in for an actual T that otherwise would have been initialized at some earlier point, usually during startup. A failure at that earlier point is usually fatal. If there is a potential for a recoverable failure, we recommend that you build the retry logic into the initialization routine (in this case, the factory method), just as you would if you weren't using lazy initialization.
作为 "workaround" - 抛弃内置的 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);
}
我正在为我的代码编写单元测试并且遇到了一个方法,该方法在尝试创建队列并将 AddMessage 添加到队列时抛出 StorageException。我想测试异常处理是否正常。为了做到这一点,我想到了为 CloudQueue 使用模拟,但后来发现这个 class 是密封的。有没有什么方法可以在不实际更改生产代码的情况下测试异常处理(或强制 StorageException)?
我们过去使用 Microsoft Fakes 框架能够对类似的 Azure SDK 类 进行单元测试。前面有一点学习曲线,但效果很好。
处理这个问题的最简单方法是使用围绕 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.
}
这就是第一个接口和包装器...请注意这个 returns 一个 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));
}
@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
的所有后续调用将 return 相同的异常。
docs 说:
Exception caching When you use factory methods, exceptions are cached. That is, if the factory method throws an exception the first time a thread tries to access the Value property of the Lazy object, the same exception is thrown on every subsequent attempt. This ensures that every call to the Value property produces the same result and avoids subtle errors that might arise if different threads get different results. The Lazy stands in for an actual T that otherwise would have been initialized at some earlier point, usually during startup. A failure at that earlier point is usually fatal. If there is a potential for a recoverable failure, we recommend that you build the retry logic into the initialization routine (in this case, the factory method), just as you would if you weren't using lazy initialization.
作为 "workaround" - 抛弃内置的 Lazy<T>
并实现你自己的。 @mariusGundersen
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);
}