首页 > 解决方案 > 如何让后备服务提供者返回编译时未知的类型参数的 Mock

问题描述

我正在为 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',因为我还没有注册一个Mock<IMySecondService>with的实例MockServiceProvider

我如何MockServiceProvider返回我没有明确注册的每种类型的模拟(类似return Mock<serviceType)?我的一些 Razor 组件有很多依赖项,我不想注入那些对每个测试都无关紧要的组件。

标签: c#blazormoqbunit

解决方案


如果根服务提供者无法解析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 测试的。


推荐阅读