Методы расширения с помощью Moq

175

У меня есть существующий интерфейс ...

public interface ISomeInterface
{
    void SomeMethod();
}

и я расширил этот интерфейс, используя миксин ...

public static class SomeInterfaceExtensions
{
    public static void AnotherMethod(this ISomeInterface someInterface)
    {
        // Implementation here
    }
}

У меня есть класс, называющий это, который я хочу проверить ...

public class Caller
{
    private readonly ISomeInterface someInterface;

    public Caller(ISomeInterface someInterface)
    {
        this.someInterface = someInterface;
    }

    public void Main()
    {
        someInterface.AnotherMethod();
    }
}

и тест, в котором я хотел бы смоделировать интерфейс и проверить вызов метода расширения ...

    [Test]
    public void Main_BasicCall_CallsAnotherMethod()
    {
        // Arrange
        var someInterfaceMock = new Mock<ISomeInterface>();
        someInterfaceMock.Setup(x => x.AnotherMethod()).Verifiable();

        var caller = new Caller(someInterfaceMock.Object);

        // Act
        caller.Main();

        // Assert
        someInterfaceMock.Verify();
    }

Запуск этого теста, однако, генерирует исключение ...

System.ArgumentException: Invalid setup on a non-member method:
x => x.AnotherMethod()

У меня вопрос, есть ли хороший способ сделать смешанный звонок?

Рассел Гиддингс
источник
3
По моему опыту, термины миксин и методы расширения - это разные вещи. Я бы использовал последнее в этом случае, чтобы избежать путаницы: P
Рубен Бартелинк
3
Дубликат: stackoverflow.com/questions/562129/… .
Оливер

Ответы:

33

Вы не можете "напрямую" макетировать статический метод (следовательно, метод расширения) с помощью фреймворка. Вы можете попробовать Moles ( http://research.microsoft.com/en-us/projects/pex/downloads.aspx ), бесплатный инструмент от Microsoft, который реализует другой подход. Вот описание инструмента:

Moles - это облегченная среда для тестирования заглушек и обходов в .NET, основанная на делегатах.

Кроты могут использоваться для обхода любого метода .NET, включая не виртуальные / статические методы в закрытых типах.

Вы можете использовать Кроты с любой средой тестирования (это не зависит от этого).

Даниэле Арманаско
источник
2
Помимо Moles, существуют и другие (несвободные) фреймворки, которые используют API профилировщика .NET для имитации объектов и могут заменять любые вызовы Двое, которых я знаю, это JustMock от Telerik и TypeMock Isolator .
Марсель Госселин
6
Кроты в теории - это хорошо, но я обнаружил три проблемы, когда я попробовал их, которые остановили меня в их использовании ... 1) Он не запускается в бегунке Resharper NUnit 2) Вам нужно вручную создать сборку моль для каждой сборочной сборки 3 ) Вам нужно вручную воссоздать сборку крота при изменении метода с заглушкой.
Рассел Гиддингс
26

Я использовал Обертку, чтобы обойти эту проблему. Создайте объект-обертку и передайте имитируемый метод.

См. Статические методы Mocking Static для модульного тестирования Пола Ирвина, там есть хорошие примеры.

Алвис
источник
12
Мне нравится этот ответ, потому что он говорит (не говоря напрямую), что вам нужно изменить свой код, чтобы сделать его тестируемым. Вот только как это работает. Поймите, что в дизайне микрочипа / IC / ASIC эти микросхемы должны быть не только предназначены для работы, но и разработаны для дальнейшего тестирования, потому что если вы не можете протестировать микрочип, он бесполезен - вы не можете гарантировать, что он будет работай. То же самое касается программного обеспечения. Если вы не создали его для тестирования, это ... бесполезно. Сделайте его тестируемым, что в некоторых случаях означает переписывание кода (и использование оберток), а затем создайте автоматизированные тесты, которые его тестируют.
Майкл
2
Я создал небольшую библиотеку, которая упаковывает Dapper, Dapper.Contrib и IDbConnection. github.com/codeapologist/DataAbstractions.Dapper
Дрю Сумидо
15

Я обнаружил, что мне нужно обнаружить внутреннюю часть метода расширения, для которого я пытался смоделировать ввод, и смоделировать то, что происходило внутри расширения.

Я рассматривал использование расширения как добавление кода непосредственно к вашему методу. Это означало, что мне нужно было высмеивать то, что происходит внутри расширения, а не само расширение.

chris31389
источник
11

Вы можете смоделировать тестовый интерфейс, который наследуется от реального и имеет элемент с такой же сигнатурой, что и у метода расширения.

Затем вы можете смоделировать тестовый интерфейс, добавить реальный в макет и вызвать метод теста в настройке.

Ваша реализация макета может затем вызвать любой метод, который вы хотите, или просто проверить, что метод вызывается:

IReal //on which some extension method is defined
{
    ... SomeRegularMethod(...);
}

static ExtensionsForIReal
{
    static ... SomeExtensionMethod(this IReal iReal,...);
}

ITest: IReal
{
    //This is a regular method with same name and signature as the extension without the "this IReal iReal" parameter
    ... SomeExtensionMethod(...);
}

var someMock = new Mock<ITest>();
Mock.As<IReal>(); //ad IReal to the mock
someMock.Setup(x => x.SomeExtensionMethod(...)).Verifiable(); //Calls SomeExtensionMethod on ITest
someMock.As<IReal>().Setup(x => x.SomeRegularMethod(...)).Verifiable(); //Calls SomeRegularMethod on IReal

Спасибо решению Håvard S в этом посте за то, как реализовать макет, который поддерживает два интерфейса. Как только я нашел его, адаптировать его к тестовому интерфейсу и статическому методу было очень просто.

pasx
источник
Можете ли вы показать нам, пожалуйста, образец подписи для SomeNotAnExtensionMethodи SomeNotAnExtensionMethod? Теперь у меня есть идея, как создать сигнатуру метода расширения внутри интерфейса ...
Питер Чала
1
@Peter Csala: Извините, если это неясно. Я обновил сообщение, чтобы сделать его более понятным, и переименовал SomeNotAnExtensionMethod в SomeRegularMethod.
Pasx
Как передать iRealпараметр SomeExtensionMethodв случае ITest? Если вы передадите его в качестве первого параметра, то как вы настраиваете? Setup( x=> x.SomeExtensionMethod(x, ...)это вызовет исключение во время выполнения.
Питер Чала
Вы не пропустите это. С точки зрения макета, ITest содержит обычный метод без параметра this, который соответствует сигнатуре вызовов метода расширения в вашем коде, поэтому вы никогда не получите здесь IReal, и в любом случае реализация IReal / ITest сама является макетом. , Если вы хотите получить доступ к некоторым свойствам фиктивного IReal в SomeExtensionMethod, вы должны сделать все это в макете, например: object _mockCache = whatever... `Setup (x => x.SomeExtensionMethod (...) .. Callback (() => вы можете доступ к _mockCache здесь);)
pasx
8

Вы можете легко смоделировать метод расширения с помощью JustMock . API - это то же самое, что и обычный метод насмешки. Рассмотрим следующее

public static string Echo(this Foo foo, string strValue) 
{ 
    return strValue; 
}

Для организации и проверки этого метода используйте следующее:

string expected = "World";

var foo = new Foo();
Mock.Arrange(() => foo.Echo(Arg.IsAny<string>())).Returns(expected);

string result = foo.Echo("Hello");

Assert.AreEqual(expected, result);

Вот также ссылка на документацию: Методы расширения Mocking

Михаил Владов
источник
2

Мне нравится использовать обертку (шаблон адаптера), когда я обертываю сам объект. Я не уверен, что использовал бы это для переноса метода расширения, который не является частью объекта.

Я использую внутреннее свойство Lazy Injectable либо типа Action, Func, Predicate, либо делегата, и разрешаю вводить (заменять) метод во время модульного теста.

    internal Func<IMyObject, string, object> DoWorkMethod
    {
        [ExcludeFromCodeCoverage]
        get { return _DoWorkMethod ?? (_DoWorkMethod = (obj, val) => { return obj.DoWork(val); }); }
        set { _DoWorkMethod = value; }
    } private Func<IMyObject, string, object> _DoWorkMethod;

Затем вы вызываете Func вместо фактического метода.

    public object SomeFunction()
    {
        var val = "doesn't matter for this example";
        return DoWorkMethod.Invoke(MyObjectProperty, val);
    }

Для более полного примера, проверьте http://www.rhyous.com/2016/08/11/unit-testing-calls-to-complex-extension-methods/

Rhyous
источник
Это хорошо, но читатели должны знать, что _DoWorkMethod - это новое поле класса, которое каждый экземпляр класса теперь должен выделить еще по одному полю. Это редко имеет значение, но иногда это зависит от количества экземпляров, которые вы выделяете за один раз. Вы можете обойти эту проблему, сделав _DoWorkMethod статическим. Недостатком является то, что если у вас есть модульные тесты, запущенные одновременно, два разных модульных теста, которые могут изменить одно и то же статическое значение, будут завершены.
zumalifeguard
-1

Так что, если вы используете Moq и хотите смоделировать результат метода Extension, тогда вы можете использовать SetupReturnsDefault<ReturnTypeOfExtensionMethod>(new ConcreteInstanceToReturn())экземпляр класса mock, у которого есть метод extension, который вы пытаетесь смоделировать.

Это не идеально, но для целей модульного тестирования это работает хорошо.

ckernel
источник
Я полагаю, что это SetReturnsDefault <T> ()
Дэвид
это никогда не возвращает конкретный экземпляр. Просто нуль в случае пользовательского класса C #!
HelloWorld