Как я могу заставить поставщика резервных услуг возвращать макет параметра типа, который неизвестен во время компиляции

#c# #blazor #moq #bunit

Вопрос:

Я пишу тест BUnit для компонента Razor, который вводит зависимости, подобные этой:

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

Я использую его так в тесте (я использую автофиксацию для предоставления параметров теста):

 @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' , так как я не зарегистрировал экземпляр a Mock<IMySecondService> с MockServiceProvider помощью .

Как мне MockServiceProvider вернуть макет каждого типа, который я не зарегистрировал эксплицитно (что-то вроде return Mock<serviceType )? Некоторые из моих компонентов бритвы имеют множество зависимостей, и я не хочу вводить те, которые не имеют значения для каждого теста.

Комментарии:

1. Я думаю, вам нужно использовать насмешливый фреймворк. Вот сообщение в блоге, в котором перечислены несколько различных опций, которые работают вместе с автофиксацией: blog.ploeh.dk/2013/01/09/NSubstituteAuto-mockingwithAutoFixture

2. Я использую Moq и. AutoFixture.AutoMoq У меня это работает для обычных проектов на C#. Моя трудность заключается в том, что я не знаю, как это сделать с BUnit TestServiceProvider , чтобы Mock по умолчанию мне возвращали, если я не зарегистрировал ничего подобного издевательского типа у поставщика услуг.

3. bUnit просто попытается использовать резервного поставщика услуг, которого вы передадите ему в первую очередь. Если резервный IServiceProvider возвращает значение null при вызове своего GetServices метода, то bUnit попытается использовать встроенного поставщика услуг.

4. Как GetServices будет работать ваша реализация в вашем IServiceProvider проекте, полностью зависит от вас, bUnit это не волнует.

5. Я пытаюсь настроить автоматическую настройку поставщика услуг по умолчанию или резервного поставщика услуг для возврата Mock<T> экземпляра для любого типа, для которого я явно не настроил его возврат. Моя проблема в том, что я не знаю, как заставить Moq делать что-то вроде return Mock<typeof(MyClass>>()

Ответ №1:

Резервный поставщик услуг, добавленный к корневому поставщику услуг bUnit, вызывается, если корневой поставщик услуг не может разрешить GetService запрос. Имея в виду эту информацию, мы можем использовать Moq (или другую платформу для насмешек) и небольшой трюк с отражением для создания резервного поставщика услуг, который на самом деле просто реализует IServiceProvider интерфейс, который будет использовать Moq для создания издевательской версии запрашиваемой службы, когда вызывается ее GetService метод.

Поставщик услуг автосервиса

Этот поставщик услуг будет использовать макет для создания макета запрошенного типа службы один раз, и все последующие запросы будут возвращены того же типа (они сохраняются в mockedTypes словаре).

Причина этого заключается в том, что вы можете получить один и тот же издевательский экземпляр типа как в тесте, так и в тестируемом компоненте, что позволяет настроить макет в тесте.

Приведенный GetMockedService ниже метод расширения позволяет легко извлечь a 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.

Комментарии:

1. Спасибо, я попробовал это, и это сработало. Это тоже научило меня кое-чему о рефлексии.