Polly retry unit test - c#

I am using polly to handle retry (see below code). How can I unit test polly retry? using xunit and 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)
}));

As suggested in the comments I recommend Simmy.
It allows you to inject exceptions, return BadRequests and etc. in order to trigger Polly's fault and resilience policies such as WaitAndRetry.
These are a few samples from the documentation.
Inject (Socket) exception
var chaosPolicy = MonkeyPolicy.InjectException(Action<InjectOutcomeOptions<Exception>>);
For example: it causes the policy to throw SocketException with a probability of 5% if enabled
var fault = new SocketException(errorCode: 10013);
var chaosPolicy = MonkeyPolicy.InjectException(with =>
with.Fault(fault)
.InjectionRate(0.05)
.Enabled()
);
Inject (BadRequest) result
var chaosPolicy = MonkeyPolicy.InjectResult(Action<InjectOutcomeOptions<TResult>>);
For example: it causes the policy to return a bad request HttpResponseMessage with a probability of 5% if enabled
var result = new HttpResponseMessage(HttpStatusCode.BadRequest);
var chaosPolicy = MonkeyPolicy.InjectResult<HttpResponseMessage>(with =>
with.Result(result)
.InjectionRate(0.05)
.Enabled()
);
Simply set the InjectionRate to 1 to guarantee a fault in your unit test.
If you want to use the InjectionRate less than 1 you can use xunit and moq chaining via SetupSequence and Moq.Language.ISetupSequentialResult. Here's an example from an blockchain challenge I had to do, I execute 4 calls in a row, so if the InjectionRate is 0.25 one of the 4 calls would trigger a Polly policy:
[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();
}
}

You can unit test this by mocking out the HttpClient and setting up your own test version of the WaitAndRetryAsync policy.
Example:
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);

Related

Grpc HealthChecks not all serving status

I would like to check healthcheck grpc server like this link
In my GrpcServer project, in startup class I've :
//HealthCheck
services.AddHealthChecks().AddCheck("self", () => HealthCheckResult.Healthy());
services.AddGrpcHealthChecks(o => {
o.Services.MapService("myservice", _ => true);
});
And
app.UseEndpoints(endpoints => {
endpoints.MapGrpcService<MyGrpcService>();
endpoints.MapGrpcHealthChecksService();
endpoints.MapHealthChecks("/health", new HealthCheckOptions() {
Predicate = _ => true,
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
});
On client side, I use Grpc.HealthCheck package :
private static async Task Main()
{
var httpClientHandler = new HttpClientHandler();
httpClientHandler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
var httpClient = new HttpClient(httpClientHandler);
var channel = GrpcChannel.ForAddress("https://localhost:5002", new GrpcChannelOptions { HttpClient = httpClient });
var client = new Health.HealthClient(channel);
var health = await client.CheckAsync(new HealthCheckRequest { Service = "myservice" });
Console.WriteLine($"Health Status: {health.Status}");
Console.WriteLine("Press a key to exit");
Console.ReadKey();
}
Like this is working fine, but if I change service name in HealthCheckRequest, I've got:
The request was aborted. The response ended prematurely, with at least
9 additional bytes expected.
Shouldn't I have a health.Status = Unknown ? Why I've this error ?
Thanks for help

Grpc token expiration handling

I'm using grpc code-first for my blazor-wasm app and I can't understand how I should handle token expiration.
As I understand it, I need an client side interceptor that will check the expiration time and make a request to the server to update.
public static WebAssemblyHostBuilder AddGrpc(this WebAssemblyHostBuilder builder)
{
builder.Services
.AddTransient(sp => sp.GetRequiredService<IHttpClientFactory>()
.CreateClient(BaseClient));
builder.Services.AddScoped(services =>
{
var baseAddressMessageHandler = services.GetRequiredService<AuthenticationHeaderHandler>(); // <= adds the authorization header
baseAddressMessageHandler.InnerHandler = new HttpClientHandler(); // <= I tried adding a custom handler, but it didn't work either
var grpcWebHandler = new GrpcWebHandler(GrpcWebMode.GrpcWeb, baseAddressMessageHandler);
var channel = GrpcChannel.ForAddress(builder.HostEnvironment.BaseAddress, new GrpcChannelOptions { HttpHandler = grpcWebHandler });
return channel;
});
return builder;
}
How to solve this problem correctly?
Solution:
builder.Services.AddScoped(services =>
{
var authManager = services.GetRequiredService<IAuthenticationManager>();
var navManager = services.GetRequiredService<NavigationManager>();
var credentials = CallCredentials.FromInterceptor(async (context, metadata) =>
{
try
{
await authManager.TryRefreshToken();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
navManager.NavigateTo("/login");
}
});
var baseAddressMessageHandler = services.GetRequiredService<AuthenticationHeaderHandler>();
baseAddressMessageHandler.InnerHandler = new HttpClientHandler();
var grpcWebHandler = new GrpcWebHandler(GrpcWebMode.GrpcWeb, baseAddressMessageHandler);
var channel = GrpcChannel.ForAddress(builder.HostEnvironment.BaseAddress, new GrpcChannelOptions
{
HttpHandler = grpcWebHandler,
Credentials = ChannelCredentials.Create(new SslCredentials(), credentials)
});
return channel;
});
In CallCredentials.FromInterceptor we can check token and update it.

How mock methods of repository in Service for Unit test

I try to have a unit test for my service, I mocked everything needed, How I can Mock repository methods that Service is calling so that has value and code is not breaking,
This is my unit test:
public async Task Updateuser_ReturnsResponse()
{
// Arrange
var request = new UpdateUserRequest()
{
Guid = new Guid("92296ac1-f8e1-489a-a312-6ea9d31d60f8"),
FirstName = "TestFirst",
LastName = "TestLast",
PhoneWork = "9495467845",
EmailWork = "test123#yahoo.com",
};
var respose = new UpdateUserResponse()
{
Success = true
};
var getGuidRequest = new GetGuidRequest()
{
Guid = request.Guid
};
var getGuidResponse = new GetGuidResponse()
{
Guid = request.Guid
};
var mockUserRepository = new Mock<IUserRepository>();
var mockAwsProxy = new Mock<IAwsProxy>();
mockUserRepository.Setup(s => s.UpdateUserAsync(request)).ReturnsAsync(respose);
mockUserRepository.Setup(i => i.GetGuidAsync(getGuidRequest)).ReturnsAsync(getGuidResponse);
var sut = new FromService.UserService(....);
// Act
var response = await sut.UpdateUserAsync(request);
// Assert
Assert.NotNull(response);
Assert.True(response.Success);
}
My problem is when calling - var response = await sut.UpdateUserAsync(request); It goese to service and this GuidResponse is empty so it break after as shows GuidResponse Null:
public async Task<UpdateUserResponse> UpdateUserAsync(UpdateUserRequest request)
{
if (request.EmailWork.HasValue() || request.Role.HasValue())
{
var GuidResponse = await userRepository.GetGuidAsync(new GetGuidRequest
{
Guid = request.Guid
});
// it breaks here because GuidResponse is Null.
if (GuidResponse.Guid != null && request.EmailWork.HasValue())
{
.......
It fails because the setup does not match what was actually given to the mock when the test was exercised.
Use It.Is<T>() to match the passed argument parameter
//...omitted for brevity
mockUserRepository
.Setup(_ => _.GetGuidAsync(It.Is<GetGuidRequest>(x => x.Guid == request.Guid)))
.ReturnsAsync(getGuidResponse);
//...omitted for brevity
assuming the mocked repository is what was injected into the SUT
Reference Moq Quickstart: Matching Arguments

Seeding WebApplicationFactory For Integration Tests Doesn't Save and Gives a 404

So I'm trying to create a integration test with preseeded data in my dbcontext, but for some reason it's not saving my entry to the database and gives me a 404 not found after I get my response back. Happy to expand or clarify anything if needed.
Cheers
[Fact]
public async Task GetPet_ReturnsResourceWithAccurateFields()
{
var fakePet = new FakePet { }.Generate();
fakePet.PetId = 1;
var appFactory = new WebApplicationFactory<StartupAutomatedTesting>()
.WithWebHostBuilder(builder =>
{
builder
.ConfigureServices(services =>
{
services.Remove(new ServiceDescriptor(typeof(MyDbContext),
typeof(MyDbContext)));
services.AddDbContext<MyDbContext>(options =>
{
options.UseInMemoryDatabase("TestDb");
});
var serviceProvider = services
.AddEntityFrameworkInMemoryDatabase()
.BuildServiceProvider();
using (var scope = serviceProvider.CreateScope())
{
var context = scope.ServiceProvider.GetRequiredService<MyDbContext>();
context.Database.EnsureCreated();
context.Pets.AddRange(fakePet);
context.SaveChanges();
}
});
});
var testingClient = appFactory.CreateClient();
var result = await testingClient.GetAsync($"/v1/Pets/{fakePet.PetId}");
var responseContent = await result.Content.ReadAsStringAsync();
var response = JsonConvert.DeserializeObject<PetDto>(responseContent);
// Assert
response.Name.Should().Be(fakePet.Name);
response.Age.Should().Be(fakePet.Age);
}
Probably because of the (intermediate) service provider you create which gets its own singleton instance of the in memory database.
Move the seeding portion after creating the appFactory and resolve the dbcontext from the service provider from the appFactory:
var appFactory = new WebApplicationFactory<StartupAutomatedTesting>()
.WithWebHostBuilder(builder =>
{
... etc ...
});
using (var scope = appFactory.Services.CreateScope())
{
var context = scope.ServiceProvider.GetRequiredService<MyDbContext>();
context.Database.EnsureCreated();
context.Pets.AddRange(fakePet);
context.SaveChanges();
}
var testingClient = appFactory.CreateClient();
... etc ...

How can I mock the Response.StatusCode with Moq?

I have the following method:
public void SetHttpStatusCode(HttpStatusCode httpStatusCode)
{
Response.StatusCode = (int)httpStatusCode;
}
And the following test:
[TestMethod]
public void SetHttpStatusCode_SetsCorrectStatusCode()
{
//Arrange
//Any url will suffice
var mockHttpContext = TestHelpers.MakeHttpContext("");
mockHttpContext.SetupSet(x => x.Response.StatusCode = It.IsAny<int>());
//creates an instance of an asp.net mvc controller
var controller = new AppController()
{
ControllerContext = new ControllerContext() {
HttpContext = mockHttpContext.Object }
};
// Act
controller.SetHttpStatusCode(HttpStatusCode.OK);
//Assert
mockHttpContext.VerifySet(x => x.Response.StatusCode = It.IsAny<int>());
}
Also, Here is MakeHttpContext
public static Mock<HttpContextBase> MakeHttpContext(string url)
{
var mockHttpContext = new Mock<HttpContextBase>();
var mockRequest = new Mock<HttpRequestBase>();
var mockResponse = new Mock<HttpResponseBase>();
var mockSession = new Mock<HttpSessionStateBase>();
//request
mockRequest.Setup(x => x.AppRelativeCurrentExecutionFilePath).Returns(url);
mockHttpContext.Setup(x => x.Request).Returns(mockRequest.Object);
//response
mockResponse.Setup(x => x.ApplyAppPathModifier(It.IsAny<string>())).Returns<string>(x => x);
mockHttpContext.Setup(x => x.Response).Returns(mockResponse.Object);
//session
mockHttpContext.Setup(x => x.Session).Returns(mockSession.Object);
return mockHttpContext;
}
When I run the test, I get the following exception:
Test method PA.Tests.Controllers.AppControllerTest.SetHttpStatusCode_SetsCorrectStatusCode
threw exception:
Moq.MockException:
Expected invocation on the mock at least once,
but was never performed: x => x.StatusCode = It.IsAny<Int32>()
Configured setups:
x => x.StatusCode = It.IsAny<Int32>(), Times.Never
No invocations performed.
How does Moq expect/require invocations to be called? I've debugged the SetHTTPStatusCode method, the response object is indeed a mocked object, however Moq insists that there was no invocation. Am I missing something?
Thanks!
You haven't shown what your TestHelpers.MakeHttpContext method does so it's a bit difficult to understand what's going on.
Try like this:
// Arrange
var mockHttpContext = new Mock<HttpContextBase>();
var response = new Mock<HttpResponseBase>();
mockHttpContext.SetupGet(x => x.Response).Returns(response.Object);
//creates an instance of an asp.net mvc controller
var controller = new AppController()
{
ControllerContext = new ControllerContext()
{
HttpContext = mockHttpContext.Object
}
};
// Act
controller.SetHttpStatusCode(HttpStatusCode.OK);
//Assert
response.VerifySet(x => x.StatusCode = (int)HttpStatusCode.OK);

Categories

Resources