#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#. Моя трудность заключается в том, что я не знаю, как это сделать с BUnitTestServiceProvider
, чтобы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. Спасибо, я попробовал это, и это сработало. Это тоже научило меня кое-чему о рефлексии.