bUnit-dev / bUnit

bUnit is a testing library for Blazor components that make tests look, feel, and runs like regular unit tests. bUnit makes it easy to render and control a component under test’s life-cycle, pass parameter and inject services into it, trigger event handlers, and verify the rendered markup from the component using a built-in semantic HTML comparer.
https://bunit.dev
MIT License
1.14k stars 105 forks source link

Support and documentation for services injected through component constructors #325

Closed alex-netkachov closed 9 months ago

alex-netkachov commented 3 years ago

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;
    }
  }
}
egil commented 3 years ago

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.

alex-netkachov commented 3 years ago

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