Configure MassTransit for testing with WebApplicationFactory<Startup> - .net-core

I have an ASP.NET Core web app and test setup using WebApplicationFactory to test my controller actions. I used RawRabbit before and it was easy enough for me to mock the IBusClient and add it to the DI container as a singleton. Within the WebApplicationFactory<TStartup>.CreateWebHostBuilder() I call this extension method to add my mocked IBusClient instance like so;
/// <summary>
/// Configures the service bus.
/// </summary>
/// <param name="webHostBuilder">The web host builder.</param>
/// <returns>A web host builder.</returns>
public static IWebHostBuilder ConfigureTestServiceBus(this IWebHostBuilder webHostBuilder)
{
webHostBuilder.ConfigureTestServices(services =>
{
services.AddSingleton<IBusClient, MY_MOCK_INSTANCE>
});
return webHostBuilder;
}
But there are gaps in RawRabbit right now that made me decide to move over to MassTransit. However, I am wondering if there's already a better way to register the IBus into my container without mocking it inside my test. Not sure if InMemoryTestFixture, BusTestFixture, or BusTestHarness is the solution to my problem. Not sure how to use them together and what they do.
By the way, in my ASP.NET Core app, I have a reusable extension method setup like the code below to hook me up to RabbitMQ on startup.
/// <summary>
/// Adds the service bus.
/// </summary>
/// <param name="services">The services.</param>
/// <param name="configurator">The configurator.</param>
/// <returns>A service collection.</returns>
public static IServiceCollection AddServiceBus(this IServiceCollection services, Action<IServiceCollectionConfigurator> configurator)
{
var rabbitMqConfig = new ConfigurationBuilder()
.AddJsonFile("/app/configs/service-bus.json", optional: false, reloadOnChange: true)
.Build();
// Setup DI for MassTransit.
services.AddMassTransit(x =>
{
configurator(x);
// Get the json configuration and use it to setup connection to RabbitMQ.
var rabbitMQConfig = rabbitMqConfig.GetSection(ServiceBusOptionsKey).Get<RabbitMQOptions>();
// Add bus to the container.
x.AddBus(provider => Bus.Factory.CreateUsingRabbitMq(cfg =>
{
cfg.Host(
new Uri(rabbitMQConfig.Host),
hostConfig =>
{
hostConfig.Username(rabbitMQConfig.Username);
hostConfig.Password(rabbitMQConfig.Password);
hostConfig.Heartbeat(rabbitMQConfig.Heartbeat);
});
cfg.ConfigureEndpoints(provider);
// Add Serilog logging.
cfg.UseSerilog();
}));
});
// Add the hosted service that starts and stops the BusControl.
services.AddSingleton<IMessageDataRepository, EncryptedMessageDataRepository>();
services.AddSingleton<IEndpointNameFormatter, EndpointNameFormatter>();
services.AddSingleton<IBus>(provider => provider.GetRequiredService<IBusControl>());
services.AddSingleton<IHostedService, BusHostedService>();
return services;
}

A MassTransit config defined during Startup could be replaced with a new configuration with custom WebApplicationFactory by removing services from MassTransit namespace, e.g.
public class CustomWebApplicationFactory : WebApplicationFactory<Startup>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
var massTransitHostedService = services.FirstOrDefault(d => d.ServiceType == typeof(IHostedService) &&
d.ImplementationFactory != null &&
d.ImplementationFactory.Method.ReturnType == typeof(MassTransitHostedService)
);
services.Remove(massTransitHostedService);
var descriptors = services.Where(d =>
d.ServiceType.Namespace.Contains("MassTransit",StringComparison.OrdinalIgnoreCase))
.ToList();
foreach (var d in descriptors)
{
services.Remove(d);
}
services.AddMassTransitInMemoryTestHarness(x =>
{
//add your consumers (again)
});
});
}
}
Then your test could look like
public class TestClass : IClassFixture<CustomApplicationFactory>
{
private readonly CustomApplicationFactoryfactory;
public TestClass(CustomApplicationFactoryfactory)
{
this.factory = factory;
}
[Fact]
public async Task TestName()
{
CancellationToken cancellationToken = new CancellationTokenSource(5000).Token;
var harness = factory.Services.GetRequiredService<InMemoryTestHarness>();
await harness.Start();
var bus = factory.Services.GetRequiredService<IBusControl>();
try
{
await bus.Publish<MessageClass>(...some message...);
bool consumed = await harness.Consumed.Any<MessageClass>(cancellationToken);
//do your asserts
}
finally
{
await harness.Stop();
}
}
}

I ended up creating a method from within my WebApplicationFactory like so;
public void ConfigureTestServiceBus(Action<IServiceCollectionConfigurator> configurator)
{
this._configurator = configurator;
}
Giving me the ability to register test handlers from within my derived integration class constructor;
public Intg_GetCustomers(WebApplicationTestFactory<Startup> factory)
: base(factory)
{
factory.ConfigureTestServiceBus(c =>
{
c.AddConsumer<TestGetProductConsumer>();
});
}
This configurator gets used when I call my extension method to add an InMemory instance of MassTransit
public static IWebHostBuilder ConfigureTestServiceBus(this IWebHostBuilder webHostBuilder, Action<IServiceCollectionConfigurator> configurator)
{
return webHostBuilder
.ConfigureTestServices(services =>
{
// UseInMemoryServiceBus DI for MassTransit.
services.AddMassTransit(c =>
{
configurator?.Invoke(c);
// Add bus to the container.
c.AddBus(provider =>
{
var control = Bus.Factory.CreateUsingInMemory(cfg =>
{
cfg.ConfigureEndpoints(provider);
});
control.Start();
return control;
});
});
services.AddSingleton<IMessageDataRepository, InMemoryMessageDataRepository>();
});
}

You best bet is to use the InMemoryTestHarness, so that you can ensure your message contracts can be serialized, your consumers are configured properly, and that everything works as expected. While some might call this an integration test, it's really just doing a proper test. And it's extremely quick, since it's all in-memory.
You can see a unit test here, but a short example is also shown below.
[TestFixture]
public class When_a_consumer_is_being_tested
{
InMemoryTestHarness _harness;
ConsumerTestHarness<Testsumer> _consumer;
[OneTimeSetUp]
public async Task A_consumer_is_being_tested()
{
_harness = new InMemoryTestHarness();
_consumer = _harness.Consumer<Testsumer>();
await _harness.Start();
await _harness.InputQueueSendEndpoint.Send(new A());
}
[OneTimeTearDown]
public async Task Teardown()
{
await _harness.Stop();
}
[Test]
public void Should_have_called_the_consumer_method()
{
_consumer.Consumed.Select<A>().Any().ShouldBe(true);
}
class Testsumer :
IConsumer<A>
{
public async Task Consume(ConsumeContext<A> context)
{
await context.RespondAsync(new B());
}
}
class A
{
}
class B
{
}
}

In my case (I'm injecting everywhere IPublishEndpoint interface only) I have just simply registered another IPublishEndpoint in ConfigureTestServices method like so:
[TestClass]
public class TastyTests
{
private readonly WebApplicationFactory<Startup> factory;
private readonly InMemoryTestHarness harness = new();
public TastyTests()
{
factory = new WebApplicationFactory<Startup>().WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddSingleton<IPublishEndpoint>(serviceProvider =>
{
return harness.Bus;
});
});
});
}
[TestMethod]
public async Task Test()
{
await harness.Start();
try
{
var client = factory.CreateClient();
const string url = "/endpoint-that-publish-message";
var content = new StringContent("", Encoding.UTF8, "application/json");
var response = await client.PostAsync(url, content);
(await harness.Published.Any<IMessage>()).Should().BeTrue();
}
finally
{
await harness.Stop();
}
}
}

Related

'HubCallerContext' does not contain a definition for 'Request' singnalr .netcore

I'm using signalr with .Net Core 3.0 and Angular 8
public class ConnectionHub : Hub
{
public ConnectionHub()
{
}
public override async Task OnConnectedAsync()
{
//get userName, token and other stuff
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception exception)
{
await base.OnDisconnectedAsync(exception);
}
}
Added in startup.cs:
app.UseSignalR(options =>
{
options.MapHub<ConnectionHub>("/ConnectionHub");
});
In angular
//configure
private connection: any = new signalR.HubConnectionBuilder().withUrl("http://localhost:6002/ConnectionHub?userName='abc'")
.configureLogging(signalR.LogLevel.Information)
.build();
//start
this.connection.start()
I want to access my custom data sent in query params, but in OnConnectedAsync, the context is following:
How can I access query params in OnConnectedAsync() method.
Documentation says I can use Context.Request, but in OnConnectedAsync it says 'HubCallerContext' does not contain a definition for 'Request'
I found the solution
Inherit ConnectionHub with Hub<IHubClients>
get request object by using Context.GetHttpContext().Request
so the ConnectionHub class will look like this:
public class ConnectionHub: Hub<IHubClients>
{
public ConnectionHub()
{
}
public override async Task OnConnectedAsync()
{
//get userName, token and other stuff
var userName = Context.GetHttpContext().Request.Query["userName"];
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception exception)
{
await base.OnDisconnectedAsync(exception);
}
}

Register a Component with IServiceCollection

I want to build my NotificationLibrary so it feels like a real .NET Core component.
This is what I want in my Startup class
public void ConfigureServices(IServiceCollection services)
{
services.AddNotificationLibrary(options =>
{
options.ConfigSectionName = "notificationSetup";
});
}
So the next steps should be to configure the NotificationLibrary with the configuration from appsetting.json and register it in DI.
This is what I have so fare
namespace Microsoft.Extensions.DependencyInjection
{
public static class NotificationLibraryFactoryServiceCollectionExtensions
{
public static IServiceCollection AddNotificationLibrary(
this IServiceCollection services,
Action<NotificationLibraryOptions> configure)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
if (configure != null)
{
services.Configure(configure);
}
services.AddScoped<IMailFactory, MailFactory>(provider =>
{
var mailFactory = new MailFactory();
var configuration = provider.GetRequiredService<IConfiguration>();
//var setup = configuration. /* How to I get the section name? */
var config = new NotificationSetup();
mailFactory.Configure(config);
return mailFactory;
});
return services;
}
}
}
I am stocked getting the config section name, and I don't understand what services.Configure(configure); are doing.
I want to create a configration poco object and add it as a singleton to the service collection. Next step would be to call mailFactory.Configure(config); and return the mailFactory object.
I am close, but what am I missing?
Resolve the options registered with the service collection
public static IServiceCollection AddNotificationLibrary(
this IServiceCollection services,
Action<NotificationLibraryOptions> configure) {
if (services == null) {
throw new ArgumentNullException(nameof(services));
}
if (configure != null) {
services.Configure(configure);
}
services.AddScoped<IMailFactory, MailFactory>(provider => {
/* How to I get the section name? */
IOptions<NotificationLibraryOptions> options = provider.GetRequiredService<IOptions<NotificationLibraryOptions>>();
string sectionName = options.Value.ConfigSectionName;
var configuration = provider.GetRequiredService<IConfiguration>();
NotificationSetup config = configuration.GetSection(sectionName).Get<NotificationSetup>();
MailFactory mailFactory = new MailFactory();
mailFactory.Configure(config);
return mailFactory;
});
return services;
}

Polly Circuit breaker not maintaining state with .net core HTTP Client

I have implemented the polly retry and Circuit breaker policy (wrapped). when the call fails and the circuit is open for the previous call the next call again goes to the retry and hit the circuit breaker again instead of just throwing the circuitbreakexception. I think somehow the HTTP client is getting recreated again even though am using the typed client. I am not able to figure the issue. Here is the code
Startup
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddHttpClient<IIntCall, IntCall>().WrapResilientPolicies();
}
Interface
public interface IIntCall
{
Task<bool> DoSomething();
}
Implementation:
public class IntCall : IIntCall
{
private readonly HttpClient client;
public IntCall(HttpClient httpClient)
{
this.client = httpClient;
}
public async Task<bool> DoSomething()
{
var response = await client.GetAsync("http://www.onegoogle.com");
var content = await response.Content.ReadAsStringAsync();
return false;
}
}
Polly Implementation
public static class CBExtensions
{
public static void WrapResilientPolicies(this IHttpClientBuilder builder)
{
builder.AddPolicyHandler((service, request) =>
GetRetryPolicy().WrapAsync(GetCircuitBreakerPolicy()));
}
private static IAsyncPolicy<HttpResponseMessage> GetCircuitBreakerPolicy()
{
return HttpPolicyExtensions.HandleTransientHttpError()
.CircuitBreakerAsync(3, TimeSpan.FromSeconds(30), (result, retryAttempt) =>
{
Debug.WriteLine("circuit broken");
},
() =>
{
Debug.WriteLine("circuit closed");
});
}
private static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
return HttpPolicyExtensions.HandleTransientHttpError()
.Or<Exception>(e => !(e is BrokenCircuitException))
.WaitAndRetryAsync(3,
retryAttempt => TimeSpan.FromMilliseconds(500),
onRetry: (context, attempt) =>
{
Debug.WriteLine("error");
}
);
}
}
I figured the issue. because I am fetching the request details the policy is injected every call and hence the state is renewed. I moved my code from
public static void WrapResilientPolicies(this IHttpClientBuilder builder)
{
builder.AddPolicyHandler((service, request) =>
GetRetryPolicy().WrapAsync(GetCircuitBreakerPolicy()));
}
to this
public static void WrapResilientPolicies(this IHttpClientBuilder builder)
{
builder.AddPolicyHandler(
GetRetryPolicy().WrapAsync(GetCircuitBreakerPolicy()));
}

Seeding Database in Asp.net Core, cannot create admin account

I'm trying to create a default admin account when I start an application.
Now what I'm interested in is how to seed a database in asp.net core. I have a seed code that I run in the main program. It shows no error but does not update the database. I've been trying to change "Identity rolls" to Application Role in my SeedData, but it has no effect at all.
I wouldn't want to change most of the code and I know it can be done with a model builder, but I don't want it that way. I think the problem is with the main program, but I don't understand what I need to change. My code is shown here.
SeedData.cs
namespace AspNetCoreTodo
{
public static class SeedData
{
public static async Task InitializeAsync(IServiceProvider services)
{
var roleManager = services.GetRequiredService<RoleManager<IdentityRole>>();
await EnsureRolesAsync(roleManager);
var userManager = services.GetRequiredService<UserManager<ApplicationUser>>();
await EnsureTestAdminAsync(userManager);
}
private static async Task EnsureRolesAsync(RoleManager<IdentityRole> roleManager)
{
var alreadyExists = await roleManager.RoleExistsAsync(Constants.AdministratorRole);
if (alreadyExists) return;
await roleManager.CreateAsync(new IdentityRole(Constants.AdministratorRole));
}
private static async Task EnsureTestAdminAsync(UserManager<ApplicationUser> userManager)
{
var testAdmin = await userManager.Users
.Where(x => x.UserName == "admin#todo.local")
.SingleOrDefaultAsync();
if (testAdmin != null) return;
testAdmin = new ApplicationUser { Email = "admin#todo.local", UserName = "admin#todo.local" };
await userManager.CreateAsync(testAdmin, "NotSecure123!!");
await userManager.AddToRoleAsync(testAdmin, Constants.AdministratorRole);
}
}
}
ApplicationDbContext.cs
namespace AspNetCoreTodo.Data
{
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
public DbSet<TodoItem> Items {get; set;}
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
// Customize the ASP.NET Identity model and override the defaults if needed.
// For example, you can rename the ASP.NET Identity table names and more.
// Add your customizations after calling base.OnModelCreating(builder);
}
}
}
Program.cs
namespace AspNetCoreTodo
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
private static void InitializeDatabase(IWebHost host)
{
using (var scope = host.Services.CreateScope())
{
var services = scope.ServiceProvider;
try
{
SeedData.InitializeAsync(services).Wait();
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred seeding the DB.");
}
}
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
}
Startup.cs //Configuration
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlite(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddControllersWithViews();
services.AddRazorPages();
services.AddMvc();
services.AddAuthentication();
services.AddScoped<ITodoItemService, TodoItemService>();
}
Does your application ever goes in the method InitializeDatabase(IWebHost host) in Program.cs?
Could you please try to call your method in Main() method:
public static void Main(string[] args)
{
var webHost = CreateWebHostBuilder(args).Build();
InitializeDatabase(webHost);
webHost.Run();
}
Note: You have to create that 'webHost' variable, because your method takes 'IWebHost' as a parameter. And CreateWebHostBuilder(string[] args) method returns type of IWebHostBuilder. Also Run() method will work on type of IWebHost.
Note: As Nilay noticed above, I'd also seed my database in Startup.cs, in the
if(env.isDevelopment){
InitializeDatabase(webHost);
}
Because normally, seeding is a "development" purpose.

FluentValidation: Mocking ValidationResult.IsValid

I'm using FluentValidation with WebAPI in DotNet core 2. I've written tests for the validator successfully, but now I'm trying to mock the validator for my controller. Controller as follows:
[Route("[controller]")]
public class SecurityController : Controller {
private readonly IValidator<AuthenticateRequest> _authenticateRequestValidator;
public SecurityController(IValidator<AuthenticateRequest> authenticateRequestValidator) {
_authenticateRequestValidator = authenticateRequestValidator;
}
[HttpPost]
[AllowAnonymous]
[Route("auth")]
public async Task<IActionResult> AuthenticateAsync([FromBody] AuthenticateRequest req) {
// Validate
var validator = await _authenticateRequestValidator.ValidateAsync(req);
if(!validator.IsValid) {
return BadRequest();
}
// ...snip
}
}
AuthenticateRequest looks like this:
public class AuthenticateRequest {
public string Username { get; set; }
public string Password { get; set; }
}
And the validator is as follows:
public class AuthenticateRequestValidator : AbstractValidator<AuthenticateRequest> {
/// <summary>
/// Provides a validator for <see cref="AuthenticateRequest" />
/// </summary>
public AuthenticateRequestValidator() {
RuleFor(x => x.Username)
.NotNull()
.NotEmpty()
.WithMessage("Username is required");
RuleFor(x => x.Password)
.NotNull()
.NotEmpty()
.WithMessage("Password is required");
}
}
It's being injected into the controller with dot net core's standard DI. Not posting code as it's not relevant to this issue, as its a testing issue.
I'm testing with xunit, Moq and AutoFixture. Here are two tests:
public class SecurityControllerTests {
private readonly IFixture Fixture = new Fixture().Customize(new AutoMoqCustomization {ConfigureMembers = true});
private readonly Mock<IValidator<AuthenticateRequest>> authenticateRequestValidatorMock;
public SecurityControllerTests() {
authenticateRequestValidatorMock = Mock.Get(Fixture.Create<IValidator<AuthenticateRequest>>());
}
[Fact]
public async Task Authenticate_ValidatesRequest() {
// Arrange
var request = Fixture.Create<AuthenticateRequest>();
authenticateRequestValidatorMock
.Setup(x => x.ValidateAsync(It.Is<AuthenticateRequest>(v => v == request), default(CancellationToken)))
.Returns(() => Fixture.Create<Task<ValidationResult>>())
.Verifiable();
var controller = new SecurityController(authenticationServiceMock.Object, tokenisationServiceMock.Object, authenticateRequestValidatorMock.Object);
// Act
await controller.AuthenticateAsync(request);
// Assert
authenticateRequestValidatorMock.Verify();
}
[Fact]
public async Task Authenticate_Returns400_WhenUsernameValidationFails() {
// Arrange
var request = Fixture.Create<AuthenticateRequest>();
var validationResultMock = new Mock<ValidationResult>();
validationResultMock
.SetupGet(x => x.IsValid)
.Returns(() => true);
authenticateRequestValidatorMock
.Setup(x => x.ValidateAsync(It.Is<AuthenticateRequest>(v => v == request), default(CancellationToken)))
.Returns(() => new Task<ValidationResult>(() => validationResultMock.Object));
var controller = new SecurityController(authenticationServiceMock.Object, tokenisationServiceMock.Object, authenticateRequestValidatorMock.Object);
// Act
var result = await controller.AuthenticateAsync(request);
// Assert
var badRequestResult = Assert.IsType<BadRequestObjectResult>(result);
Assert.IsType<SerializableError>(badRequestResult.Value);
}
}
I need to mock ValidationResult so I can ignore the actual validator logic (which is tested elsewhere) and test the controller logic. There are many other dependencies injected, and much more code, but the pasted code is the crux of the problem and produces the same results when everything else is stripped out.
First test passes, second runs forever when it hits the var validator = await _authenticateRequestValidator.ValidateAsync(req); line in the controller.
Its worth noting that ValidationResult.IsValid is a virtual readonly property.
What is wrong with the second test?
Did you try Asp.Net Core - FluentValidation integration? By this way you dont need to pass Validator depedencies to constructor.
https://github.com/JeremySkinner/FluentValidation/wiki/i.-ASP.NET-Core-integration
FluentValidation fills ModelState in case of validation error and you use it like ;
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
For testing it you set ModelState of you Controllers Mock
var controller = new SecurityController(authenticationServiceMock.Object, tokenisationServiceMock.Object, authenticateRequestValidatorMock.Object);
controller.ModelState.AddModelError("test", "test");
// Act
IActionResult actionResult = await controller.AuthenticateAsync(request);
var badRequestObjectResult = actionResult as BadRequestObjectResult;
Assert.NotNull(badRequestObjectResult);
var serializableError = badRequestObjectResult.Value as SerializableError;
// Assert
Assert.NotNull(result);
var badRequestResult = Assert.IsType<BadRequestObjectResult>(result);
var serializableError = assert.IsType<SerializableError>(badRequestResult.Value)
Assert.True(((string[])serializableError["test"])[0] == "test");
Leaving ModelState empty would be enough to ignore the actual validator logic i think.
Also FluentValidation have built-in testing api. You can test your validation logic separately.
https://github.com/JeremySkinner/FluentValidation/wiki/g.-Testing

Resources