При издевательстве над классом с помощью Moq, как я могу вызвать CallBase только для определенных методов? - PullRequest
35 голосов
/ 13 мая 2010

Мне очень нравится Moq's Loose насмешливое поведение, которое возвращает значения по умолчанию, когда ожидания не установлены. Это удобно и экономит мой код, а также действует как мера безопасности: зависимости не будут вызываться непреднамеренно во время модульного теста (пока они виртуальные).

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

Все, что я нашел в своем поиске, - это то, что я могу установить mock.CallBase = true, чтобы метод вызывался. Однако это влияет на весь класс. Я не хочу этого делать, потому что это ставит меня перед дилеммой по поводу всех других свойств и методов в классе, которые скрывают зависимости вызовов: если CallBase равен true, тогда мне нужно либо

  1. Настройка заглушки для всех свойств и методов, которые скрывают зависимости - хотя мой тест не считает, что нужно заботиться об этих зависимостях, или
  2. Надеюсь, что я не забуду установить любые заглушки (и что в будущем в код не будут добавлены новые зависимости) - модульные тесты риска достигают реальной зависимости.

Я думаю, что хочу что-то вроде:
mock.Setup(m => m.VirtualMethod()).CallBase();
так что когда я вызываю mock.Object.VirtualMethod(), Moq вызывает реальную реализацию ...

В: С Moq, есть ли способ протестировать виртуальный метод, когда я высмеял класс, чтобы заглушить несколько зависимостей? То есть Не прибегая к CallBase = true и без необходимости заглушить все зависимости?


Пример кода для иллюстрации
(использует MSTest, InternalsVisibleTo DynamicProxyGenAssembly2)

В следующем примере TestNonVirtualMethod проходит, но TestVirtualMethod терпит неудачу - возвращает ноль.

public class Foo
{
    public string NonVirtualMethod() { return GetDependencyA(); }
    public virtual string VirtualMethod() { return GetDependencyA();}

    internal virtual string GetDependencyA() { return "! Hit REAL Dependency A !"; }
    // [... Possibly many other dependencies ...]
    internal virtual string GetDependencyN() { return "! Hit REAL Dependency N !"; }
}

[TestClass]
public class UnitTest1
{
    [TestMethod]
    public void TestNonVirtualMethod()
    {
        var mockFoo = new Mock<Foo>();
        mockFoo.Setup(m => m.GetDependencyA()).Returns(expectedResultString);

        string result = mockFoo.Object.NonVirtualMethod();

        Assert.AreEqual(expectedResultString, result);
    }

    [TestMethod]
    public void TestVirtualMethod() // Fails
    {
        var mockFoo = new Mock<Foo>();
        mockFoo.Setup(m => m.GetDependencyA()).Returns(expectedResultString);
        // (I don't want to setup GetDependencyB ... GetDependencyN here)

        string result = mockFoo.Object.VirtualMethod();

        Assert.AreEqual(expectedResultString, result);
    }

    string expectedResultString = "Hit mock dependency A - OK";
}

Ответы [ 3 ]

10 голосов
/ 12 августа 2015

Я думаю, что ответ Лунивора был верным на момент написания.

В более новых версиях Moq (я думаю, начиная с версии 4.1 от 2013 года) можно делать то, что вы хотите, с точным синтаксисом, который вы предлагаете,То есть:

mock.Setup(m => m.VirtualMethod()).CallBase();

Это устанавливает свободный макет для вызова базовой реализации VirtualMethod вместо простого возврата default(WhatEver), но только для этого члена (VirtualMethod).


Как отмечает пользователь BornToCode в комментариях, это не будет работать, если метод имеет тип возвращаемого значения void.Когда VirtualMethod не является пустым, вызов Setup дает Moq.Language.Flow.ISetup<TMock, TResult>, который наследует метод CallBase() от Moq.Language.Flow.IReturns<TMock, TResult>.Но когда метод является недействительным, мы получаем Moq.Language.Flow.ISetup<TMock> вместо которого отсутствует требуемый CallBase() метод.

8 голосов
/ 18 октября 2010

Поскольку никто не отвечал на этот вопрос целую вечность, и я думаю, что он заслуживает ответа, я сосредоточусь на вопросе самого высокого уровня, который вы задали: как сохранить эти преимущества, когда тестируемый метод оказывается виртуальным.

Быстрый ответ: вы не можете сделать это с Moq, или, по крайней мере, не напрямую. Но вы можете сделать это.

Допустим, у вас есть два аспекта поведения, где аспект A является виртуальным, а аспект B - нет. Это в значительной степени отражает то, что вы имеете в своем классе. B может использовать другие методы или нет; вам решать.

В данный момент ваш класс Foo выполняет две вещи - и А, и Б. Я могу сказать, что они являются отдельными обязанностями только потому, что вы хотите отключить А и протестировать В самостоятельно.

Вместо того, чтобы пытаться смоделировать виртуальный метод, не выдумывая ничего другого, вы можете:

  • перенести поведение A в отдельный класс
  • Зависимость вводит новый класс с A в конструктор Foo через Foo
  • вызвать этот класс из B.

Теперь вы можете смоделировать A и по-прежнему вызывать реальный код для B..N, фактически не вызывая реальное A. Вы можете оставить виртуальный A или получить к нему доступ через интерфейс и смоделировать его. Это также соответствует принципу единой ответственности.

Вы можете каскадировать конструкторы с помощью Foo - заставить конструктор Foo() вызывать конструктор Foo(new A()) - так что вам даже не понадобится структура внедрения зависимостей для этого.

Надеюсь, это поможет!

0 голосов
/ 22 июля 2017

Там - это способ вызова реального метода и с обратным вызовом, когда метод void, но это действительно хакер. Вы должны сделать так, чтобы ваш обратный вызов вызывал это явно и обманом заставлял Moq вызывать настоящий.

Например, для данного класса

public class MyInt
{
    public bool CalledBlah { get; private set; }

    public virtual void Blah()
    {
        this.CalledBlah = true;
    }
}

Вы можете написать свой тест следующим образом:

[Test]
public void Test_MyRealBlah()
{
    Mock<MyInt> m = new Mock<MyInt>();
    m.CallBase = true;

    bool calledBlah = false;
    m.When(() => !calledBlah)
        .Setup(i => i.Blah())
        .Callback(() => { calledBlah = true; m.Object.Blah(); })
        .Verifiable();

    m.Object.Blah();

    Assert.IsTrue(m.Object.CalledBlah);
    m.VerifyAll();
}

Ключевым аспектом является то, что вы отслеживаете, была ли вызвана фальшивая версия, а затем вы устанавливаете макет на , а не вызывает фальшивку, если она уже была вызвана.

Вы все еще можете сделать нечто подобное, если вы берете аргументы и значение имеет значение:

public class MyInt
{
    public List<int> CalledBlahArgs { get; private set; }

    public MyInt()
    {
        this.CalledBlahArgs = new List<int>();
    }

    public virtual void Blah(int a)
    {
        this.CalledBlahArgs.Add(a);
    }
}

[Test]
public void Test_UpdateQueuedGroups_testmockcallback()
{
    Mock<MyInt> m = new Mock<MyInt>();
    m.CallBase = true;

    List<int> fakeBlahArgs = new List<int>();

    m.Setup(i => i.Blah(It.Is<int>(a => !fakeBlahArgs.Contains(a))))
        .Callback<int>((a) => { fakeBlahArgs.Add(a); m.Object.Blah(a); })
        .Verifiable();

    m.Object.Blah(1);

    Assert.AreEqual(1, m.Object.CalledBlahArgs.Single());
    m.Verify(b => b.Blah(It.Is<int>(id => 1 == id)));
}
...