簡體   English   中英

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

[英]Mocking generic methods in Moq without specifying T

我有一個帶有方法的接口,如下所示:

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

我想模擬包含此方法的類,而不必為它可以用於的每種類型指定 Setup 方法。 理想情況下,我只想返回一個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.
}

被測對象的某些方法可能會為三種或四種不同類型調用儲備。 如果我必須設置所有類型,我必須為每個測試編寫大量設置代碼。 但是在單個測試中,我並不關心所有這些,我只需要非空的模擬對象,除了我實際測試的對象(我很樂意為其編寫更復雜的設置)。

在 Moq 4.13 中,他們引入了 It.IsAnyType 類型,您可以使用它來模擬泛型方法。 例如

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

只需這樣做:

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

  ...
}

由於您的模擬沒有行為Strict ,它會對您甚至沒有設置的調用感到滿意。 在這種情況下,只會返回“默認值”。 然后

DefaultValue.Mock

確保這個“默認”是一個適當類型的新Mock<> ,而不僅僅是一個空引用。

這里的限制是您無法控制(例如對其進行特殊設置)返回的各個“子模擬”。

除非我誤解了您的需求,否則您可以構建這樣的方法:

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

我找到了一個我認為更接近你想要的選擇。 無論如何,它對我很有用,所以在這里。 這個想法是創建一個幾乎完全抽象的中間類,並實現您的接口。 不抽象的部分是 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上編寫設置之外,一切都相同。 如果您想根據類型執行斷言,您可以處理回調,盡管ReserveProxyType參數是完全可選的。

這是一種似乎有效的方法。 如果您在 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 方法之外的其他版本。

我找不到任何關於使用 Moq 使用通用模擬方法模擬通用方法的信息。 到目前為止,我發現的唯一一件事是模擬每個特定類型的泛型方法,這無濟於事,因為一般來說,您無法真正提前預見泛型參數的所有可能情況/變化。

所以我通過創建我自己的那個接口的假/空實現來解決這種問題,而不是使用 Moq。

在您的情況下,它看起來像這樣:

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

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

之后,只需注入那個虛假的實現來代替 IRepo 的使用。

在我的例子中,我有一個泛型函數 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());
        }
    }
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM