Подтвердить что ты не робот

Когда насмехается над классом с Moq, как я могу использовать CallBase для определенных методов?

Я очень ценю Moq Loose mocking поведение, которое возвращает значения по умолчанию, когда ожидания не установлены. Это удобно и сохраняет код, а также действует как мера безопасности: зависимости не будут непреднамеренно вызваны во время unit test (пока они виртуальны).

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

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

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

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

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


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

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

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";
}
4b9b3361

Ответ 1

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

В более новых версиях 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().

Обновление: andrew.rockwell ниже отмечает, что теперь оно работает для методов void и, по-видимому, это было исправлено в версии 4.10 (с 2018 года).

Ответ 2

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

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

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

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

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

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

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

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

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

Ответ 3

Существует способ вызова реального метода и возврата вызова, когда метод 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)));
}