c# - 具有 NSubstitute 的 AutoFixture 是否可能/支持从代理接口(interface)返回自动值

标签 c# mocking autofixture nsubstitute

最近试用的时候AutoFixture as an NSubstitute auto-mocking container我在实现过程中遇到了一个似乎令人惊讶的缺陷。尽管替代似乎是为指定为接口(interface)类型的构造函数/工厂参数自动生成的,但生成的替代/模拟似乎并没有像我预期的那样自动配置为返回通过固定装置指定的自动值。

为了说明我认为开箱即用的方法,我在下面创建了一个简单的测试。

    [Test]
    public void MyClass_WhenAskedToDoSomething_ShouldReturnANumberFromSomeService()
    {
        // Arrange
        var fixture = new Fixture().Customize(new AutoNSubstituteCustomization());
        var expectedNumber = fixture.Freeze<int>();

        var sut = fixture.Create<MyClass>();

        // Act
        var number = sut.AskToDoSomething();

        // Assert
        Assert.IsTrue(number == expectedNumber);
    }

    public class MyClass
    {
        private readonly IMyInterface _myInterface;

        public MyClass(IMyInterface myInterface)
        {
            _myInterface = myInterface;
        }

        public int AskToDoSomething()
        {
            return _myInterface.GetService().GetNumber();
        }
    }

    public interface IMyInterface
    {
        ISomeService GetService();
    }

    public interface ISomeService
    {
        int GetNumber();   
    }

如果我期待的是 AutoNSubstituteCustomization 实现中没有包含的东西,那么像这样的东西会很难实现吗?有没有其他人朝这个方向前进。任何指针都会有所帮助。

最佳答案

自从我自己试了一下之后,我想我应该发布一些我到目前为止的想法。以下是一组允许将广泛的默认值功能应用于单个 NSubstitute 替代实例的类型。

public interface IDefaultValueFactory
{
    T GetDefault<T>();
}

public static class NSubstituteDefaultValueConfigurator
{
    public static void Configure(Type substituteType, object substitute, IDefaultValueFactory valueFactory)
    {
        var type = typeof(NSubstituteDefaultValueConfigurator<>)
            .MakeGenericType(substituteType);

        var configurator = type
            .GetConstructor(new Type[] { typeof(IDefaultValueFactory) })
            .Invoke(new object[] { valueFactory });

        type.GetMethod("ConfigureDefaultReturnValuesForAllMethods")
            .Invoke(configurator, new object[] { substitute });
    }
}


public class NSubstituteDefaultValueConfigurator<T>
{
    private readonly IDefaultValueFactory _valueFactory;

    public NSubstituteDefaultValueConfigurator(IDefaultValueFactory valueFactory)
    {
        _valueFactory = valueFactory;
    }

    private object GetDeafultValue<TResult>()
    {
        return _valueFactory.GetDefault<TResult>();
    }


    public void ConfigureDefaultReturnValuesForAllMethods(T substitute)
    {
        var interfaces = substitute
            .GetType()
            .GetInterfaces()
            // HACK: Specifically exclude supporting interfaces from NSubstitute
            .Where(i =>
                i != typeof(Castle.DynamicProxy.IProxyTargetAccessor) &&
                i != typeof(ICallRouter) /*&&
                i != typeof(ISerializable)*/)
            .ToArray();

        var methods = interfaces
            .SelectMany(i => i.GetMethods())
            .Where(m => m.ReturnType != typeof(void))

            // BUG: skipping over chained interfaces in NSubstitute seems
            // to cause an issue with embedded returns. Using them however
            // causes the mock at the end or along a chained call not to be
            // configured for default values.
            .Where(m => !m.ReturnType.IsInterface);

        foreach (var method in methods)
        {
            var typedConfigureMethod = this
                .GetType()
                .GetMethod("ConfigureDefaultReturnValuesForMethod", BindingFlags.NonPublic | BindingFlags.Static)
                .MakeGenericMethod(method.ReturnType);

            var defaultValueFactory = new Func<CallInfo, object>(
                callInfo => this
                    .GetType()
                    .GetMethod("GetDeafultValue", BindingFlags.NonPublic | BindingFlags.Instance)
                    .MakeGenericMethod(method.ReturnType)
                    .Invoke(this, null));

            typedConfigureMethod.Invoke(
                this,
                new object[]
                    {
                        substitute, 
                        defaultValueFactory,
                        method
                    });
        }

        //var properties = interfaces.SelectMany(i => i.GetProperties());
        var properties = substitute
            .GetType().GetProperties();

        foreach (var property in properties)
        {
            var typedConfigureMethod = this
                .GetType()
                .GetMethod("ConfigureDefaultReturnValuesForProperty", BindingFlags.NonPublic | BindingFlags.Static)
                .MakeGenericMethod(property.PropertyType);

            var defaultValueFactory = new Func<CallInfo, object>(
                callInfo => this
                    .GetType()
                    .GetMethod("GetDeafultValue", BindingFlags.NonPublic | BindingFlags.Instance)
                    .MakeGenericMethod(property.PropertyType)
                    .Invoke(this, null));

            typedConfigureMethod.Invoke(
                this,
                new object[]
                    {
                        substitute, 
                        defaultValueFactory,
                        property
                    });
        }
    }

    private static void ConfigureDefaultReturnValuesForMethod<TResult>(
        T substitute,
        Func<CallInfo, object> defaultValueFactory,
        MethodInfo method)
    {
        var args = method
            .GetParameters()
            .Select(p => GetTypedAnyArg(p.ParameterType))
            .ToArray();

        // Call the method on the mock
        var substituteResult = method.Invoke(substitute, args);

        var returnsMethod = typeof(SubstituteExtensions)
            .GetMethods(BindingFlags.Static | BindingFlags.Public)
            .First(m => m.GetParameters().Count() == 2)
            .MakeGenericMethod(method.ReturnType);

        var typedDefaultValueFactory = new Func<CallInfo, TResult>(callInfo => (TResult)defaultValueFactory(callInfo));

        returnsMethod.Invoke(null, new[] { substituteResult, typedDefaultValueFactory });
    }

    private static void ConfigureDefaultReturnValuesForProperty<TResult>(
        T substitute,
        Func<CallInfo, object> defaultValueFactory,
        PropertyInfo property)
    {
        // Call the property getter on the mock
        var substituteResult = property.GetGetMethod().Invoke(substitute, null);

        var returnsMethod = typeof(SubstituteExtensions)
            .GetMethods(BindingFlags.Static | BindingFlags.Public)
            .First(m => m.GetParameters().Count() == 2)
            .MakeGenericMethod(property.PropertyType);

        var typedDefaultValueFactory = new Func<CallInfo, TResult>(callInfo => (TResult)defaultValueFactory(callInfo));

        returnsMethod.Invoke(null, new[] { substituteResult, typedDefaultValueFactory });
    }

    private static object GetTypedAnyArg(Type argType)
    {
        return GetStaticGenericMethod(typeof(Arg), "Any", argType);
    }

    private static MethodInfo GetStaticGenericMethod(
        Type classType,
        string methodName,
        params Type[] typeParameters)
    {
        var method = classType
            .GetMethod(methodName, BindingFlags.Static | BindingFlags.Public)
            .MakeGenericMethod(typeParameters);

        return method;
    }
}

由于需要为每个单独的替代实例调用 Configure 方法,因此需要对 AutoFixture AutoNSubstitute 支持类中的支持类进行一些侵入式修改,或者需要提供 AutoNSubstitute 的替代实现。在我直接修改 AutoNSubstitute 源代码时,我修改了 NSubstituteBuilder类如下以使其具有可配置的默认/自动值功能。

    public object Create(object request, ISpecimenContext context)
    {
        if (!SubstitutionSpecification.IsSatisfiedBy(request))
            return new NoSpecimen(request);

        var substitute = Builder.Create(request, context);
        if (substitute == null)
            return new NoSpecimen(request);

        NSubstituteDefaultValueConfigurator.Configure(
            substitute.GetType(), 
            substitute,
            new AutoFixtureDefaultValueFactory(context));

        return substitute;
    }

    private class AutoFixtureDefaultValueFactory : IDefaultValueFactory
    {
        private readonly ISpecimenContext _context;

        public AutoFixtureDefaultValueFactory(ISpecimenContext context)
        {
            _context = context;
        }

        public T GetDefault<T>()
        {
            return _context.Create<T>();
        }
    }

不幸的是,我的实现中存在一个错误,该错误处理对替代品上属性 getter 的反射调用,或者 NSubstitute 处理属性的方式与方法不同,但无论哪种方式,我都遇到了一些障碍。剩下的问题是,对于链式接口(interface)(从其成员返回其他接口(interface)的接口(interface)),当在叶属性调用中遇到应通过 AutoFixture 解析的具体类时,会抛出 CouldNotSetReturnException。这似乎只发生在属性而不是方法上,尽管这既有趣又不幸。鉴于 NSubsitute Returns method design 中似乎存在限制以及 configuring default values more broadly 的通用 API 中的限制.

所以在这一点上,答案似乎是否定的,开箱即用的 AutoFixture 的 AutoNSubstitute 自定义不支持通过返回的替代成员返回由夹具返回的相同自动值的功能。另一方面,AutoFixture 的维护者似乎愿意接受并可能支持此功能的合理实现,并且我已经能够证明我可以使用 NSubstitute 的可用设施至少实现部分工作的实现,而无需修改。

作为旁注,一个对我来说似乎很明显的模式是使用静态工厂创建模拟并且没有任何类型的基于实例的上下文的模拟库自然缺乏配置生成的模拟行为的能力测试。当我第一次在单元测试中采用模拟时,我很早就想到了这个限制,这是它第一次给我带来问题。

关于c# - 具有 NSubstitute 的 AutoFixture 是否可能/支持从代理接口(interface)返回自动值,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/24272164/

相关文章:

unit-testing - Autofixture 和 WebApi Controller

c# - 类中的 AutoFixture 设置界面属性

c# - 如何实现搜索?

groovy - 如何让多个 MockFor 在 Groovy 中工作?

Python:模拟模块而不导入它或不需要它存在

reactjs - Jest 手动模拟在 CRA 中不使用模拟文件

.net - Autofixture 奇怪的错误

c# - 使用 nUnit 和 nMocks 进行单元测试

c# - 完整组满足条件的 LINQ 查询

c# - AppDomain 卸载杀死父 AppDomain