在Moq中模拟泛型方法而不指定T

98

我有一个具有以下方法的接口:

public interface IRepo
{
    IA<T> Reserve<T>();
}

我希望能够模拟包含此方法的类,而无需为它可能使用的每种类型都指定设置方法。理想情况下,我只想让它返回一个 new mock<T>.Object

我该如何实现这一点?

看起来我的解释不够清楚。以下是一个示例 - 当我指定 T(这里是字符串)时,现在就可以实现:

[TestMethod]
public void ExampleTest()
{
    var mock = new Mock<IRepo>();
    mock.Setup(pa => pa.Reserve<string>()).Returns(new Mock<IA<string>>().Object);
}

我希望实现的是像这样的效果:

[TestMethod]
public void ExampleTest()
{
    var mock = new Mock<IRepo>();
    mock.Setup(pa => pa.Reserve<T>()).Returns(new Mock<IA<T>>().Object);
    // of course T doesn't exist here. But I would like to specify all types
    // without having to repeat the .Setup(...) line for each of them.
}

在测试对象的某些方法中,可能会调用三到四种不同类型的reserve。如果我必须为每个测试设置所有类型,那么我就需要编写很多设置代码。但是在单个测试中,我并不关心它们中的所有类型,我只需要非空的模拟对象,除了我实际正在测试的对象(对于这个对象,我很乐意编写更复杂的设置)。

7个回答

75
在 Moq 4.13 中引入了 It.IsAnyTypeIt.IsSubtype<T> 类型, 您可以使用它们来模拟泛型方法。例如:
public interface IFoo
{
    bool M1<T>();
    bool M2<T>(T arg);
}

var mock = new Mock<IFoo>();
// matches any type argument:
mock.Setup(m => m.M1<It.IsAnyType>()).Returns(true);

// matches only type arguments that are subtypes of / implement T:
mock.Setup(m => m.M1<It.IsSubtype<T>>()).Returns(true);

// use of type matchers is allowed in the argument list:
mock.Setup(m => m.M2(It.IsAny<It.IsAnyType>())).Returns(true);
mock.Setup(m => m.M2(It.IsAny<It.IsSubtype<T>>())).Returns(true);

请注意,此方法无法与返回泛型的方法一起使用,详见此处解释。我们还讨论了一个可能的解决方法。

5
这个东西发生了什么?它不再可用了吗? - Michael Puckett II
@MichaelPuckettII 不,它在Moq 4.15中仍然有效。我认为其他答案是在这个版本之前被接受的。 - Mark Wells
16
如果您想要设置的方法返回T,则此方法无效。 - Matus
1
@Matus 我在模拟一个具有泛型参数 T 约束的通用方法时遇到了麻烦(例如:where T: Foo)。根据我的理解,在这种情况下没有办法模拟泛型类型参数。我错了吗? - Enrico Massone

31

除非我误解了您所需的内容,否则您可以构建如下方法:

private Mock<IRepo> MockObject<T>()
{
    var mock = new Mock<IRepo>();
    return mock.Setup(pa => pa.Reserve<T>())
        .Returns(new Mock<IA<T>>().Object).Object;
}

我尝试了这个,但是当我尝试编译时,我一直收到“找不到'T'类型或命名空间”的错误。 - Wilbert
6
当然,区别在于你将整个内容放在了一个通用函数中。我需要将它放在一个非通用函数中。 - Wilbert
@Wilbert,如果不将函数泛型化,你无法封装所需的代码。通过我上面的编辑,现在你可以这样做:var mock = MockObject<MyConcreteType>();,然后就会得到一个实现该类型的Mock<IRepo> - Mike Perrenoud
4
我理解,但我需要这个代码仓库支持几种不同的类型。看起来唯一的方法是编写大量的 .Setup 方法 :( - Wilbert
1
哈哈。啊好吧,我只是希望在Moq中有些东西能为我节省粗活。 - Wilbert

31

只需这样做:

[TestMethod]
public void ExampleTest()
{
  var mock = new Mock<IRepo> { DefaultValue = DefaultValue.Mock, };
  // no setups needed!

  ...
}

由于你的模拟没有 Strict 行为,它会接受你甚至没有设置的调用,并且会简单地返回一个“默认值”。

DefaultValue.Mock

确保这个“默认值”是适当类型的新Mock<>,而不仅仅是一个空引用。

这里的限制是您无法控制(例如,在返回的各个“子模拟”上进行特殊设置)。


当您不关心自动生成的模拟对象上属性的空值时,此解决方案是可行的,因为您无法影响/设置这些创建的对象。 - Mladen B.

9

我找到了一个替代方案,我认为它更接近您的要求。无论如何,这对我很有用,下面让我来介绍一下。这个思路是创建一个中间类,该类几乎是纯抽象的,并实现您的接口。 Moq无法处理的部分不是抽象的部分。例如:

public abstract class RepoFake : IRepo
{
    public IA<T> Reserve<T>()
    {
        return (IA<T>)ReserveProxy(typeof(T));
    }

    // This will be mocked, you can call Setup with it
    public abstract object ReserveProxy(Type t);

    // TODO: add abstract implementations of any other interface members so they can be mocked
}

现在你可以模拟RepoFake而不是IRepo。除了你将你的设置写在ReserveProxy上而不是Reserve上之外,一切都一样。如果你想要根据类型执行断言,则可以处理回调,尽管Type参数对ReserveProxy来说完全可选。

1
这里有一种看起来可行的方法。如果你在 IRepo 中使用的所有类都继承自一个基类,那么你可以直接使用此方法而无需更新它。
public Mock<IRepo> SetupGenericReserve<TBase>() where TBase : class
{
    var mock = new Mock<IRepo>();
    var types = GetDerivedTypes<TBase>();
    var setupMethod = this.GetType().GetMethod("Setup");

    foreach (var type in types)
    {
        var genericMethod = setupMethod.MakeGenericMethod(type)
            .Invoke(null,new[] { mock });
    }

    return mock;
}

public void Setup<TDerived>(Mock<IRepo> mock) where TDerived : class
{
    // Make this return whatever you want. Can also return another mock
    mock.Setup(x => x.Reserve<TDerived>())
        .Returns(new IA<TDerived>());
}

public IEnumerable<Type> GetDerivedTypes<T>() where T : class
{
    var types = new List<Type>();
    var myType = typeof(T);

    var assemblyTypes = myType.GetTypeInfo().Assembly.GetTypes();

    var applicableTypes = assemblyTypes
        .Where(x => x.GetTypeInfo().IsClass 
                && !x.GetTypeInfo().IsAbstract 
                 && x.GetTypeInfo().IsSubclassOf(myType));

    foreach (var type in applicableTypes)
    {
        types.Add(type);
    }

    return types;
}

否则,如果您没有基类可以修改SetupGenericReserve,不使用TBase类型参数,而是创建一个要设置的所有类型的列表,类似于以下内容:
public IEnumerable<Type> Alternate()
{
    return new [] 
    {
        MyClassA.GetType(),
        MyClassB.GetType()
    }
}

注意:本文是针对ASP.NET Core编写的,但除了GetDerivedTypes方法外,其他版本也适用。


0

我在使用Moq时无法找到有关如何使用通用模拟方法模拟通用方法的任何信息。到目前为止,我唯一找到的是针对特定类型模拟通用方法,但这并没有帮助,因为通常情况下,您无法预见所有可能的通用参数情况/变化。

因此,我通过创建自己的接口的虚假/空实现来解决了这种问题,而不是使用Moq。

在您的情况下,它应该是这样的:

public interface IRepo
{
    IA<T> Reserve<T>();
}

public class FakeRepo : IRepo
{
    public IA<T> Reserve<T>()
    {
        // your logic here
    }
}

然后,只需将该虚假实现注入到IRepo使用的位置。


-1
在我的情况下,被测试类调用了一个名为begin的通用函数。由于被测试类决定具体类型,我无法将其传递给测试。我找到了一个解决方案,即为被测试类使用的每个具体类型分别调用setup两次,这比解释要容易得多。
顺便说一句,我正在使用Moq.AutoMock库。
using Xunit;
using System;
using Moq;
using Moq.AutoMock;

namespace testexample
{
    public class Foo
    {
        // This is the generic method that I need to mock
        public virtual T Call<T>(Func<T> f) => f();
    }

    public class Bar
    {
        private readonly Foo Foo;
        public Bar(Foo foo)
        {
            Foo = foo;
        }

        public string DoSomething()
        {
            // Here I call it twice, with distinct concrete types.
            var x1 = Foo.Call<string>(() => "hello");
            var x2 = Foo.Call<int>(() => 1);
            return $"{x1} {x2}";
        }
    }

    public class UnitTest1
    {
        [Fact]
        public void Test1()
        {
            var mock = new AutoMocker();

            // Here I setup Call twice, one for string
            // and one for int.
            mock.Setup<Foo, string>(x => x.Call<string>(It.IsAny<Func<string>>()))
                .Returns<Func<string>>(f => "mocked");
            mock.Setup<Foo, int>(x => x.Call<int>(It.IsAny<Func<int>>()))
                .Returns<Func<int>>(f => 2000);

            var bar = mock.CreateInstance<Bar>();

            // ... and Voila!
            Assert.Equal("mocked 2000", bar.DoSomething());
        }

        [Fact]
        public void Test2()
        {
            var bar = new Bar(new Foo());
            Assert.Equal("hello 1", bar.DoSomething());
        }
    }
}


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接