3

最近,在尝试将AutoFixture 作为 NSubstitute 自动模拟容器时,我遇到了在实现中似乎令人惊讶的缺陷。尽管替代似乎是为指定为接口类型的构造函数/工厂参数自动生成的,但生成的替代/模拟似乎并没有像我预期的那样自动配置为返回通过夹具指定的自动值。

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

    [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 实现中不包含某些内容,那么这样的事情是否难以实现。有没有其他人朝这个方向迈进。任何指针都会有所帮助。

4

1 回答 1

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>();
        }
    }

不幸的是,在我的实现中存在一个错误,它处理对替代品上的属性获取器的反射调用,或者 NSubstitute 在处理属性方面与方法不同,但无论哪种方式我都遇到了一些障碍。剩下的问题是,对于链式接口(从其成员返回其他接口的接口),当在叶属性调用上遇到应该通过 AutoFixture 解决的具体类时,会引发 CouldNotSetReturnException。这似乎只发生在属性而不是方法上,尽管这既有趣又不幸。考虑到NSubsitute Returns 方法设计中的一个限制以及更广泛地配置默认值的通用 API 的限制。

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

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

于 2014-06-25T11:16:07.343 回答