我怎样才能有一个后备服务提供者 return 一个在编译时未知的类型参数的 Mock

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

我正在为 Razor 组件编写 BUnit 测试,该组件已注入如下依赖项:

@inject IMyFirstService FirstService
@inject IMySecondService SecondService

@code {

   // do stuff 

}

为了我的测试,我创建了一个后备服务提供者 MoqServiceProvider,我用它来注册我的模拟依赖项。 但我还希望后备服务提供商默认为我没有明确模拟的任何类型提供一个 Moq<MyType> 实例。

后备服务提供商看起来像这样

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

我在测试中这样使用它(我使用 AutoFixture 提供测试参数):

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

}

如果我 运行 这个,我会得到一个错误 Cannot provide a value for property 'SecondService' on type 'MyComponent' 因为我还没有用 MockServiceProvider.[=21 注册一个 Mock<IMySecondService> 的实例=]

我如何获得 MockServiceProvider return 我没有显式注册的每种类型的模拟(类似于 return Mock<serviceType)?我的一些 Razor 组件有很多依赖关系,我不想每次测试都注入无关紧要的组件。

如果根服务提供者无法解决 GetService 请求,将调用添加到 bUnit 根服务提供者的回退服务提供者。考虑到这些信息,我们可以使用 Moq(或另一个模拟框架)和一些反射技巧来创建一个后备服务提供者,它实际上只是实现 IServiceProvider 接口的东西,它将使用 Moq 创建一个请求服务的模拟版本,当它的 GetService 方法被调用时。

AutoMockingServiceProvider

此服务提供者将使用 Mock 为请求的服务类型创建一次模拟,任何后续请求都将返回相同的类型(它们保存在 mockedTypes 字典中)。

这样做的原因是您可以在测试和被测组件中检索同一类型的模拟实例,这允许您在测试中配置模拟。

下面的GetMockedService扩展方法可以很容易地从服务提供商那里拉出一个Mock<T>

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>()!);
}

注意:此代码不处理任何边缘情况,因此它可能无法在所有情况下工作,但应该作为一个很好的起点。

用法示例

假设我们有以下组件:

@inject IPerson Person
@Person.Name

这取决于这个接口:

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

然后可以这样测试:

[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");
}

已使用 .NET 6 rc.1、Moq 4.16.1 和 bunit 1.2.49 进行测试。