模仿 Moq 的泛型方法,但没有指定 t

我有一个带有如下方法的接口:

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 以支持三种或四种不同的类型。如果我必须设置所有的类型,我必须为每个测试编写大量的设置代码。但是在一个单独的测试中,我并不关心所有的对象,我只需要非空的模拟对象,除了我实际测试的对象(我很高兴为其编写了更复杂的设置)。

80497 次浏览

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

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

The limitation here is that you cannot control (e.g. make special setups on) the individual "sub-mocks" that are returned.

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. E.g.

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. Everything works the same except for you write your setups on ReserveProxy instead of Reserve. You can handle the Callback if you want to perform assertions based on type, though the Type parameter to ReserveProxy is totally optional.

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.

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:

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.

I couldn't find any information on mocking the generic methods with a generic mock method, using 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.

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.

In Moq 4.13 they introduced the It.IsAnyType type which you can using to mock generic methods. E.g.

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

In my case I had a generic function begin called by the tested class. 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.

BTW I'm using Moq.AutoMock library

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