在使用NSubstitute的AutoFixture中是否可以/支持从代理接口返回自动值

时间:2014-06-17 19:49:11

标签: c# mocking autofixture nsubstitute

最近在尝试AutoFixture as an NSubstitute auto-mocking container时,我遇到了一个令人惊讶的实施缺陷。虽然替换似乎是为指定为接口类型的构造函数/工厂参数自动生成的,但生成的替换/模拟似乎不会自动配置为返回通过夹具指定的自动值,如我所料。

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

    [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实现中,那么这样的事情很难实现。有没有人朝这个方向走过去。任何指针都会有所帮助。

1 个答案:

答案 0 :(得分:0)

由于我自己已经采取了这种做法,因此我认为我应该发布一些我迄今为止所提出的内容。以下是一组类型,允许将宽泛的默认值功能应用于单个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处理属性的方法不同于方法,但是我遇到了一些障碍。剩下的问题是,对于链接接口(从其成员返回其他接口的接口),当在叶子属性调用中遇到应通过AutoFixture解析的具体类时,抛出CouldNotSetReturnException。这似乎只发生在属性而不是方法中,虽然这既有趣又不幸。鉴于NSubsitute Returns method design中的限制似乎是configuring default values more broadly的一般API中的限制。

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

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