Polly重试单元测试



我正在使用polly来处理重试(请参阅下面的代码(。如何进行单元测试polly重试?使用xunit和moq

services.AddHttpClient("GitHub", client =>
{
client.BaseAddress = new Uri("https://api.github.com/");
client.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
})
.AddTransientHttpErrorPolicy(builder => builder.WaitAndRetryAsync(new[]
{
TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(5),
TimeSpan.FromSeconds(10)
}));

正如评论中所建议的,我推荐Simmy。

它允许您注入异常、返回BadRequests等,以触发Polly的故障和恢复策略,如WaitAndRetry

这些是文档中的一些示例。

注入(套接字(异常

var chaosPolicy = MonkeyPolicy.InjectException(Action<InjectOutcomeOptions<Exception>>);

例如:如果启用,它会导致策略抛出SocketException,概率为5%

var fault = new SocketException(errorCode: 10013);
var chaosPolicy = MonkeyPolicy.InjectException(with =>
with.Fault(fault)
.InjectionRate(0.05)
.Enabled()
);

注入(BadRequest(结果

var chaosPolicy = MonkeyPolicy.InjectResult(Action<InjectOutcomeOptions<TResult>>);

例如:如果启用,它会导致策略返回错误的请求HttpResponseMessage,概率为5%

var result = new HttpResponseMessage(HttpStatusCode.BadRequest);
var chaosPolicy = MonkeyPolicy.InjectResult<HttpResponseMessage>(with =>
with.Result(result)
.InjectionRate(0.05)
.Enabled()
);

只需将InjectionRate设置为1即可保证单元测试中出现故障。


如果要使用小于1的InjectionRate,可以通过SetupSequenceMoq.Language.ISetupSequentialResult使用xunit和moq链接。这里有一个我不得不做的区块链挑战的例子,我连续执行4个调用,所以如果InjectionRate为0.25,那么4个调用中的一个将触发Polly策略:

[Fact]
public async Task Should_Return_GetEthereumTransactionsAsync()
{
// Arrange
IConfiguration settings = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build();
IOptions<Settings> optionSettings = Options.Create(new Settings
{
CompanyKeyAPI = settings.GetSection("CompanyKeyAPI").Value,
ProjectId = settings.GetSection("ProjectId").Value
});
var sequenceHttpResponse = new List<Tuple<HttpStatusCode, HttpContent>>
{
new Tuple<HttpStatusCode, HttpContent>(HttpStatusCode.OK, ApiCompanyKeyResponses.EthereumBlockWithTransactionHashs()),
new Tuple<HttpStatusCode, HttpContent>(HttpStatusCode.OK, ApiCompanyKeyResponses.Transaction(1)),
new Tuple<HttpStatusCode, HttpContent>(HttpStatusCode.OK, ApiCompanyKeyResponses.Transaction(2)),
new Tuple<HttpStatusCode, HttpContent>(HttpStatusCode.OK, ApiCompanyKeyResponses.Transaction(3))
};
IHttpClientFactory httpClientFactory = base.GetChainedCompanyKeyHttpClientFactory(new Uri(Path.Combine(optionSettings.Value.CompanyKeyAPI, optionSettings.Value.ProjectId)), sequenceHttpResponse);
CompanyKeyService CompanyKeyService = new CompanyKeyService(httpClientFactory);
// Act
List<EthereumTransaction> ethereumTransactionsResult = CompanyKeyService.GetEthereumTransactionsAsync(blockNumber, address).Result;
// Assert
Assert.IsType<List<EthereumTransaction>>(ethereumTransactionsResult);
Assert.NotNull(ethereumTransactionsResult);
Assert.Equal(ethereumTransactionsResult.Count, 3);
Assert.Equal(ethereumTransactionsResult[0].result.blockHash, blockHash);
}

public IHttpClientFactory GetChainedCompanyKeyHttpClientFactory(Uri uri, List<Tuple<HttpStatusCode, HttpContent>> httpReturns, HttpStatusCode statusCode = HttpStatusCode.OK)
{
Mock<HttpMessageHandler> httpMsgHandler = new Mock<HttpMessageHandler>();
var handlerPart = httpMsgHandler.Protected().SetupSequence<Task<HttpResponseMessage>>("SendAsync", new object[2]
{
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>()
});
foreach (var httpResult in httpReturns)
{
handlerPart = AddReturnPart(handlerPart, httpResult.Item1, httpResult.Item2);
}
httpMsgHandler.Verify();
HttpClient client = new HttpClient(httpMsgHandler.Object)
{
BaseAddress = uri
};
Mock<IHttpClientFactory> clientFactory = new Mock<IHttpClientFactory>();
clientFactory.Setup((IHttpClientFactory cf) => cf.CreateClient(It.IsAny<string>())).Returns(client);

return clientFactory.Object;
}
private Moq.Language.ISetupSequentialResult<Task<HttpResponseMessage>> AddReturnPart(Moq.Language.ISetupSequentialResult<Task<HttpResponseMessage>> handlerPart,
HttpStatusCode statusCode, HttpContent content)
{
return handlerPart
// prepare the expected response of the mocked http call
.ReturnsAsync(new HttpResponseMessage()
{
StatusCode = statusCode, 
Content = content
});
}

public class CompanyKeyService : ICompanyKeyService
{
private readonly IHttpClientFactory _clientFactory;
private readonly HttpClient _client;
public CompanyKeyService(IHttpClientFactory clientFactory)
{
_clientFactory = clientFactory;
_client = _clientFactory.CreateClient("GitHub");
}
public async Task<List<EthereumTransaction>> GetEthereumTransactionsAsync(string blockNumber, string address)
{
//Validation removed...

List<string> transactionHashs = await GetEthereumTransactionHashsByBlockNumberAsync(blockNumber);
if (transactionHashs == null) throw new Exception("Invalid entry. Please check the Block Number.");
var tasks = transactionHashs.Select(hash => GetTransactionByHashAsync(hash, address));
EthereumTransaction[] lists = await Task.WhenAll(tasks);
return lists.Where(item => item != null).ToList();
}
}

您可以通过模拟HttpClient并设置自己的测试版本的WaitAndRetryAsync策略来对其进行单元测试。示例:

var mockHttpClient = new Mock<HttpClient>();
var mockRetryPolicy = new Mock<IAsyncPolicy<HttpResponseMessage>>();
mockRetryPolicy
.Setup(p => p.ExecuteAsync(It.IsAny<Func<Context, CancellationToken, Task<HttpResponseMessage>>>(), It.IsAny<Context>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new HttpResponseMessage());
var services = new ServiceCollection();
services
.AddHttpClient("GitHub", client =>
{
client.BaseAddress = new Uri("https://api.github.com/");
client.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
})
.AddTransientHttpErrorPolicy(builder => mockRetryPolicy.Object);
var serviceProvider = services.BuildServiceProvider();
var httpClientFactory = serviceProvider.GetRequiredService<IHttpClientFactory>();
var httpClient = httpClientFactory.CreateClient("GitHub");
Assert.NotNull(httpClient);

最新更新