简体   繁体   English

在 Moq 中模拟通用方法而不指定 T

[英]Mocking generic methods in Moq without specifying T

I have an interface with a method as follows:我有一个带有方法的接口,如下所示:

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

I would like to mock the class that contains this method without having to specify Setup methods for every type it could be used for.我想模拟包含此方法的类,而不必为它可以用于的每种类型指定 Setup 方法。 Ideally, I'd just like it to return a new mock<T>.Object .理想情况下,我只想返回一个new mock<T>.Object

How do I achieve this?我如何实现这一目标?

It seems my explanation was unclear.看来我的解释不清楚。 Here's an example - this is possible right now, when I specify the T (here, string):这是一个示例 - 当我指定 T(此处为字符串)时,这现在是可能的:

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

What I would like to achieve is something like this:我想实现的是这样的:

[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.
}

Some methods of the object under test might call reserve for three or four different types.被测对象的某些方法可能会为三种或四种不同类型调用储备。 If I have to setup all the types I have to write a lot of setup code for every test.如果我必须设置所有类型,我必须为每个测试编写大量设置代码。 But in a single test, I am not concerned with all of them, I simply need non-null mocked objects except for the one that I am actually testing (and for which I gladly write a more complex setup).但是在单个测试中,我并不关心所有这些,我只需要非空的模拟对象,除了我实际测试的对象(我很乐意为其编写更复杂的设置)。

In Moq 4.13 they introduced the It.IsAnyType type which you can using to mock generic methods.在 Moq 4.13 中,他们引入了 It.IsAnyType 类型,您可以使用它来模拟泛型方法。 Eg例如

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

Simply do this:只需这样做:

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

  ...
}

Since your mock does not have behavior Strict , it will be happy with calls that you haven't even set up.由于您的模拟没有行为Strict ,它会对您甚至没有设置的调用感到满意。 In that case a "default" is simply returned.在这种情况下,只会返回“默认值”。 Then然后

DefaultValue.Mock

ensures that this "default" is a new Mock<> of appropriate type, instead of just a null reference.确保这个“默认”是一个适当类型的新Mock<> ,而不仅仅是一个空引用。

The limitation here is that you cannot control (eg make special setups on) the individual "sub-mocks" that are returned.这里的限制是您无法控制(例如对其进行特殊设置)返回的各个“子模拟”。

Unless I'm misunderstand what you need, you could build a method like this:除非我误解了您的需求,否则您可以构建这样的方法:

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

I have found an alternative that I think gets closer to what you want.我找到了一个我认为更接近你想要的选择。 Anyway it was useful for me so here goes.无论如何,它对我很有用,所以在这里。 The idea is to create an intermediate class which is almost purely abstract, and implements your interface.这个想法是创建一个几乎完全抽象的中间类,并实现您的接口。 The part which is not abstract is the part Moq can't handle.不抽象的部分是 Moq 无法处理的部分。 Eg例如

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
}

Now you can mock RepoFake instead of IRepo.现在你可以模拟RepoFake而不是 IRepo。 Everything works the same except for you write your setups on ReserveProxy instead of Reserve .除了您在ReserveProxy而不是Reserve上编写设置之外,一切都相同。 You can handle the Callback if you want to perform assertions based on type, though the Type parameter to ReserveProxy is totally optional.如果您想根据类型执行断言,您可以处理回调,尽管ReserveProxyType参数是完全可选的。

Here's one way to do it which seems to work.这是一种似乎有效的方法。 If all the classes you're using in IRepo inherit from a single base class you can use this as-is and never have to update it.如果您在 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;
}

Otherwise, if you don't have a base class you can modify the SetupGenericReserve to not use the TBase type parameter and instead just create a list of all the types you want to set up, something like this:否则,如果您没有基类,您可以修改 SetupGenericReserve 以不使用 TBase 类型参数,而只是创建您要设置的所有类型的列表,如下所示:

public IEnumerable<Type> Alternate()
{
    return new [] 
    {
        MyClassA.GetType(),
        MyClassB.GetType()
    }
}

Note: This is written for ASP.NET Core, but should work in other versions except for the GetDerivedTypes method.注意:这是为 ASP.NET Core 编写的,但应该适用于除 GetDerivedTypes 方法之外的其他版本。

I couldn't find any information on mocking the generic methods with a generic mock method, using Moq.我找不到任何关于使用 Moq 使用通用模拟方法模拟通用方法的信息。 The only thing I found so far was mocking the generic methods per-specific-type, which is not helping, because, in general, you can't really foresee all the possible cases/variations of generic parameters in advance.到目前为止,我发现的唯一一件事是模拟每个特定类型的泛型方法,这无济于事,因为一般来说,您无法真正提前预见泛型参数的所有可能情况/变化。

So I resolved this kind of issue by creating my own fake/empty implementation of that interface, instead of using Moq.所以我通过创建我自己的那个接口的假/空实现来解决这种问题,而不是使用 Moq。

In your case, it would look like this:在您的情况下,它看起来像这样:

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

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

Afterwards, just inject that fake implementation in place of IRepo usage.之后,只需注入那个虚假的实现来代替 IRepo 的使用。

In my case I had a generic function begin called by the tested class.在我的例子中,我有一个泛型函数 begin 被测试类调用。 Since the tested class that decides the concrete type I can't pass it on the test.由于决定具体类型的测试类我无法通过测试。 I found a solution where I just call setup twice, once for each concrete type being used by the tested class, it's easier to show than to explain.我找到了一个解决方案,我只调用 setup 两次,对于测试类使用的每个具体类型一次,显示比解释更容易。

BTW I'm using Moq.AutoMock library顺便说一句,我正在使用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());
        }
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM