I have the next class that I need to create a xunit test, but I don't know how mock the cosmosclient constructor.
public class CosmosDb : ICosmosDb
{
private readonly CosmosClient _client;
private readonly CosmosConfig _cosmosConfig;
private readonly Container _container;
private readonly ILogger<ICosmosDb> _logger;
/// <summary>
///
/// </summary>
/// <param name="cosmosConfig"></param>
public CosmosDb(ILogger<ICosmosDb> logger,IOptions<CosmosConfig> cosmosConfig)
{
_logger = logger;
_cosmosConfig = cosmosConfig.Value;
_client = new CosmosClient(_cosmosConfig.ConnectionString);
_container = _client.GetContainer(_cosmosConfig.Database, _cosmosConfig.ContainerId);
}
/// <summary>Creates a document in the database.</summary>
public async Task CreateItemAsync(Item doc)
{
try
{
await this.RetryUntilNot429Async(async () => await _container.CreateItemAsync<JObject>(JObject.FromObject(doc)));
}
catch (CosmosException cosmosException)
{
_logger.LogError(cosmosException,$"Exception while {nameof(CreateItemAsync)}");
throw;
}
}
/// <summary>
/// Retries the specified method indefinitely until no longer getting a
/// 429 "Too Many Requests" exception after waiting the recommended
/// delay.
/// </summary>
private async Task<T> RetryUntilNot429Async<T>(Func<Task<T>> method)
{
while (true)
{
try
{
return await method();
}
catch (CosmosException dce)
when (dce.StatusCode == (HttpStatusCode)429)
{
await Task.Delay((int)dce.RetryAfter.Value.TotalMilliseconds);
}
catch (AggregateException ae)
when ((ae.InnerException is CosmosException dce) && (dce.StatusCode == (HttpStatusCode)429))
{
await Task.Delay((int)dce.RetryAfter.Value.TotalMilliseconds);
}
}
}
}
But when create a xunit test and try to create one using the cosmosclient constructor, it demand a valid url in connection string, and when put one, of course cointainer say don't exist , so how can I mock it?? `
public class CosmosDbTest
{
[Fact]
public async Task CreateItemAsync_RetrysOn429Error()
{
// Arrange
var mockContainer = new Mock<Container>();
var mockLogger = new Mock<ILogger<CosmosDb>>();
var mockConfig = new Mock<IOptions<CosmosConfig>>();
var mockCliente = new Mock<CosmosClient>();
mockConfig.Setup(x => x.Value.ContainerId).Returns("containerid");
mockConfig.Setup(x => x.Value.Database).Returns("database");
mockConfig.Setup(x => x.Value.ConnectionString).Returns("conectionstring");
var item = new Item { Id = "123", Action = "update", BatchId = Guid.NewGuid().ToString() };
var itemJObject = JObject.FromObject(item);
var statusCode = System.Net.HttpStatusCode.TooManyRequests;
var cosmosException = new CosmosException("429 Too Many Requests", statusCode, 0, "0", 0);
mockCliente.Setup(x => x.GetContainer(It.IsAny<string>(), It.IsAny<string>())).Returns(mockContainer.Object);
mockContainer.Setup(x => x.CreateItemAsync<JObject>(itemJObject, null, null, default)).ThrowsAsync(cosmosException);
var cosmosDB = new CosmosDb(mockLogger.Object, mockConfig.Object);
// Act
await cosmosDB.CreateItemAsync(item);
// Assert
mockContainer.Verify(x => x.CreateItemAsync<JObject>(itemJObject, null, null, default), Times.Exactly(2));
}
}
First of all, when RetryUntilNot429Async
is accessible within your test project it is way easier to test the behavior of that method without needing to mock the whole cosmos db stuff.
That said, inject the CosmosClient
! Do not create it manually like you do currently as you can now not inject a mocked CosmosClient. To be able to inject it you can register the CosmosClient
as a Singleton.
builder.Services.AddSingleton((s) => {
string endpoint = configuration["EndPointUrl"];
if (string.IsNullOrEmpty(endpoint))
{
throw new ArgumentNullException("Please specify a valid endpoint in the appSettings.json file or your Azure Functions Settings.");
}
string authKey = configuration["AuthorizationKey"];
if (string.IsNullOrEmpty(authKey) || string.Equals(authKey, "Super secret key"))
{
throw new ArgumentException("Please specify a valid AuthorizationKey in the appSettings.json file or your Azure Functions Settings.");
}
CosmosClientBuilder configurationBuilder = new CosmosClientBuilder(endpoint, authKey);
return configurationBuilder
.Build();
});
(source)
Your code can use the injected instance:
public CosmosDb(ILogger<ICosmosDb> logger,CosmosClient client)
{
_logger = logger;
_cosmosConfig = cosmosConfig.Value;
_client = client;
_container = _client.GetContainer(_cosmosConfig.Database, _cosmosConfig.ContainerId);
}
Then use the parameterless constructor for mocking, see the docs