bUnit
bUnit copied to clipboard
Support and documentation for services injected through component constructors
I'm using the approach described at https://github.com/dotnet/aspnetcore/issues/18088 so my components' dependencies are injected through constructor parameters.
namespace modelx.app.ui
{
public partial class Project : IAutoRegisteredComponent
{
private readonly ILogger _logger;
[ActivatorUtilitiesConstructor]
public Project(
ContextLoggerFactory contextLoggerFactory)
{
_logger = contextLoggerFactory.Invoke(GetType());
What is the correct way in bUnit to instantiate and test such components? It would be great to add it to the documentation.
Relevant initialisation code:
using System;
using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
// ReSharper disable once CheckNamespace
namespace modelx.app
{
public interface IAutoRegisteredComponent
{
}
public partial class Program
{
// https://github.com/dotnet/aspnetcore/issues/18088
private static void Components(IServiceCollection services)
{
services.Replace(ServiceDescriptor.Transient<IComponentActivator, ServiceProviderComponentActivator>());
var autoRegisteredComponentTypes =
Assembly.GetExecutingAssembly().GetTypes()
.Where(p => typeof(IAutoRegisteredComponent).IsAssignableFrom(p) && p.IsClass);
foreach (var type in autoRegisteredComponentTypes)
services.AddTransient(type);
}
}
public class ServiceProviderComponentActivator : IComponentActivator
{
private readonly IServiceProvider _serviceProvider;
public ServiceProviderComponentActivator(IServiceProvider serviceProvider) =>
_serviceProvider = serviceProvider;
public IComponent CreateInstance(Type componentType)
{
var instance = _serviceProvider.GetService(componentType) ??
Activator.CreateInstance(componentType);
if (instance is not IComponent component)
throw new ArgumentException(
$"The type {componentType.FullName} does not implement {nameof(IComponent)}.",
nameof(componentType));
return component;
}
}
}
Hi @AlexAtNet
Interesting solution. I have not tried this before, but bUnit's TestContext has a Services collection that should work just like the IServiceCollection
in your Programs.Components
method, so the you should be able to similarly register your own IComponentActivator
with bUnit:
[Fact]
public void Test()
{
var ctx = new TestContext();
RegisterComponents(ctx.Services);
ctx.Services.Replace(ServiceDescriptor.Transient<IComponentActivator, ServiceProviderComponentActivator>());
var cut = ctx.RenderComponent<...>();
// ...
}
private static void RegisterComponents(IServiceCollection services)
{
services.Replace(ServiceDescriptor.Transient<IComponentActivator, ServiceProviderComponentActivator>());
var autoRegisteredComponentTypes =
Assembly.GetExecutingAssembly().GetTypes()
.Where(p => typeof(IAutoRegisteredComponent).IsAssignableFrom(p) && p.IsClass);
foreach (var type in autoRegisteredComponentTypes)
services.AddTransient(type);
}
I dont see any reason this should not work, as long as your component activator also works with "regular" components do not use constructor injection, but instead use the normal property based injection in Blazor.
Fantastic, it works just as expected. Thank you very much! Would be great to add this to the documentation.