Когда насмехается над классом с 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";
}
Ответы
Ответ 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)));
}