Search code examples
c#blazormoqbunit

How can I have a fallback service provider return a Mock of a type parameter that is not known at compile time


I'm writing a BUnit test for a Razor component that has injected dependencies like this:

@inject IMyFirstService FirstService
@inject IMySecondService SecondService

@code {

   // do stuff 

}

For my test, I created a fallback service provider MoqServiceProvider that I'm using to register my mocked dependencies. But I also want the fallback service provider to provide a Moq<MyType> instance by default for any types which I haven't explicitly mocked.

The fallback service provider looks like this

public class MockServiceProvider : IServiceProvider
{
    private readonly Dictionary<Type, object> services = new();

    public void RegisterServices(params object[] serviceInstances)
    {
        this.services.Clear();

        foreach (object serviceInstance in serviceInstances)
        {
            if (serviceInstance is Mock)
            {
                this.services.Add(serviceInstance.GetType().GetGenericArguments().First(), (serviceInstance as Mock).Object);
            }
            else
            {
                this.services.Add(serviceInstance.GetType(), serviceInstance);
            }
        }
    }

    public object GetService(Type serviceType)
    {
        if (this.services.TryGetValue(serviceType, out object service))
        {
            return service;
        }
        else
        {
            // I want to return a Mock of serviceType here
            // something like return new Mock<serviceType>(), but I don't know how to do that
        }
    }
}

I'm using it like this in a test (I'm using AutoFixture to provide the test parameters):

@inherits TestContext

@code{

    [Theory, AutoDomainData]
    public void TestSomething(
       Mock<IMyFirstService> myFirstService, 
       MockServiceProvider serviceProvider)
    {
       serviceProvider.RegisterServices(myFirstService);
       Services.AddFallbackServiceProvider(serviceProvider);
       var component = Render(@<MyComponent />);
   
       // do stuff to test
    }

}

If I run this, I'll get an error Cannot provide a value for property 'SecondService' on type 'MyComponent' since I haven't registered an instance of a Mock<IMySecondService> with MockServiceProvider.

How do I have MockServiceProvider return a mock of every type which I haven't registered explicity (something like return Mock<serviceType)? Some of my Razor components have a lot of dependencies and I don't want to have to inject the ones that don't matter for each test.


Solution

  • A fallback service provider added to bUnit's root service provider is invoked if the root service provider cannot resolve a GetService request. With that info in mind, we can use Moq (or another mocking framework) and a little Reflection trickery to create a fallback service provider, which is really just something that implements the IServiceProvider interface, that will use Moq to create a mocked version of a requested service, when its GetService method is called.

    AutoMockingServiceProvider

    This service provider will use Mock to create a mock of a requested service type once, and any subsequent requests will get the same type returned (they are saved in the mockedTypes dictionary).

    The reason for this is that you can retrieve the same mocked instance of a type in both your test and in the component under test, which allows you to configure the mock in your test.

    The GetMockedService extension method below makes it easy to pull out a Mock<T> from the service provider.

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Reflection;
    using Microsoft.Extensions.DependencyInjection;
    using Moq;
    
    public class AutoMockingServiceProvider : IServiceProvider
    {
        private static readonly MethodInfo GenericMockFactory = typeof(Mock).GetMethods().First(x => x.Name == "Of");
        private readonly Dictionary<Type, object> mockedTypes = new();
    
        public object? GetService(Type serviceType) => GetMockedService(serviceType);
    
        public object GetMockedService<T>() => GetMockedService(typeof(T));
    
        public object GetMockedService(Type serviceType)
        {
            if (!mockedTypes.TryGetValue(serviceType, out var service))
            {
                var mockFactory = GenericMockFactory.MakeGenericMethod(serviceType);
                service = mockFactory.Invoke(null, Array.Empty<object>())!;
                mockedTypes.Add(serviceType, service);
            }
    
            return service;
        }
    }
    
    internal static class ServiceProviderExtensions
    {
        public static Mock<T> GetMockedService<T>(this IServiceProvider services)
            where T : class => Mock.Get<T>(services.GetService<T>()!);
    }
    

    NOTE: This code doesn't deal with any edge cases, so it might not work in all cases, but should serve as a good starting point.

    Example usage

    Suppose we have the following component:

    @inject IPerson Person
    @Person.Name
    

    That depends on this interface:

    public interface IPerson
    {
        public string Name { get; }
    }
    

    Then it can be tested like this:

    [Fact]
    public void Test1()
    {
        using var ctx = new TestContext();
    
        // Add the AutoMockingServiceProvider as the fallback service provider
        ctx.Services.AddFallbackServiceProvider(new AutoMockingServiceProvider());
    
        // Retrieves the mocked person from the service collection and configures it.            
        var mockedPerson = ctx.Services.GetMockedService<IPerson>();
        mockedPerson.SetupGet(x => x.Name).Returns("Foo Bar");
    
        // Render component
        var cut = ctx.RenderComponent<MyComp>();
    
        // Verify content
        cut.MarkupMatches("Foo Bar");
    }
    

    This was tested with .NET 6 rc.1, Moq 4.16.1, and bunit 1.2.49.