bUnit icon indicating copy to clipboard operation
bUnit copied to clipboard

Support and documentation for services injected through component constructors

Open alex-netkachov opened this issue 4 years ago • 2 comments

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;
    }
  }
}

alex-netkachov avatar Feb 10 '21 17:02 alex-netkachov

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.

egil avatar Feb 10 '21 17:02 egil

Fantastic, it works just as expected. Thank you very much! Would be great to add this to the documentation.

alex-netkachov avatar Feb 11 '21 00:02 alex-netkachov