如何让回退服务提供程序返回编译时未知的类型参数的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',因为我没有注册MockServiceProviderMock<IMySecondService>实例。

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

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

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和unit 1.2.49.

最新更新