Проверка метода была вызвана
Используя Moq, у меня возникла очень странная проблема, когда настройка макета, похоже, работает, только если метод, который я настраиваю, является общедоступным. Я не знаю, является ли это ошибкой Moq или я просто ошибаюсь (новичок в Moq). Вот тестовый пример:
public class TestClass
{
public string Say()
{
return Hello();
}
internal virtual string Hello()
{
return "";
}
}
[TestMethod]
public void Say_WhenPublic_CallsHello()
{
Mock<TestClass> mock = new Mock<TestClass>();
mock.Setup(x => x.Hello()).Returns("Hello World");
string result = mock.Object.Say();
mock.Verify(x => x.Hello(), Times.Exactly(1));
Assert.AreEqual("Hello World", result);
}
Который не с этим сообщением:
Ошибка Say_WhenPublic_CallsHello: Moq.MockException: Вызов не был выполнен на макете 1 раз: x => x.Hello() в Moq.Mock.ThrowVerifyException(ожидается IProxyCall, выражение Expression, Times times)...
Если я сделаю метод Hello общедоступным, тест пройден. В чем здесь проблема?
public virtual string Hello()
{
return "";
}
Заранее спасибо!
3 ответа
Тест не проходит, когда Hello()
является внутренним, потому что Moq не может обеспечить переопределение метода в этом случае. Это означает, что внутренняя реализация Hello()
будет работать, а не макет версии, вызывая Verify()
терпеть неудачу.
Кстати, то, что вы здесь делаете, не имеет смысла в контексте модульного теста. Юнит тест не должен заботиться о том, что Say()
вызывает внутренний Hello()
метод. Это внутренняя реализация вашей сборки, а не проблема потребления кода.
Я не знаю достаточно о том, как это работает под одеялом, чтобы дать вам технический ответ относительно того, почему именно так происходит, но я думаю, что могу помочь с вашей путаницей.
В вашем примере вы вызываете метод Say(), и он возвращает ожидаемый текст. Ваше ожидание не должно обеспечивать конкретную реализацию Say(), т. Е. Что он вызывает внутренний метод Hello() для возврата этой строки. Вот почему он не проходит проверку, а также почему возвращается строка "", то есть была вызвана фактическая реализация Hello().
Делая метод Hello общедоступным, кажется, что это позволило Moq перехватить вызов к нему и использовать вместо этого его реализацию. Таким образом, в этом случае тест, кажется, проходит. Однако в этом сценарии вы не достигли ничего полезного, потому что ваш тест говорит, что когда вы вызываете Say(), результатом является "Hello World", тогда как на самом деле результат "".
Я переписал ваш пример, чтобы показать, как я ожидал использовать Moq (не обязательно окончательный, но, надеюсь, понятный).
public interface IHelloProvider
{
string Hello();
}
public class TestClass
{
private readonly IHelloProvider _provider;
public TestClass(IHelloProvider provider)
{
_provider = provider;
}
public string Say()
{
return _provider.Hello();
}
}
[TestMethod]
public void WhenSayCallsHelloProviderAndReturnsResult()
{
//Given
Mock<IHelloProvider> mock = new Mock<IHelloProvider>();
TestClass concrete = new TestClass(mock.Object);
//Expect
mock.Setup(x => x.Hello()).Returns("Hello World");
//When
string result = concrete.Say();
//Then
mock.Verify(x => x.Hello(), Times.Exactly(1));
Assert.AreEqual("Hello World", result);
}
В моем примере я представил интерфейс для IHelloProvider. Вы заметите, что реализация IHelloProvider отсутствует. Это лежит в основе того, чего мы пытаемся достичь с помощью насмешливого решения.
Мы пытаемся протестировать класс (TestClass), который зависит от чего-то внешнего (IHelloProvider). Если вы используете Test Driven Development, то, возможно, вы еще не написали IHelloProvider, но вы знаете, что он вам понадобится в какой-то момент. Вы хотите, чтобы TestClass сначала работал, а не отвлекался. Или, возможно, IHelloProvider использует базу данных или простой файл, или его сложно настроить.
Даже если у вас полностью работающий IHelloProvider, вы все еще пытаетесь протестировать поведение TestClass, поэтому использование конкретного HelloProvider, вероятно, сделает ваш тест более подверженным сбоям, например, если есть изменения в поведении HelloProvider, Вы не хотите менять тесты каждого класса, который его использует, вы просто хотите изменить тесты HelloProvider.
Возвращаясь к коду, теперь у нас есть класс TestClass, который зависит от интерфейса IHelloProvider, реализация которого предоставляется во время построения (это внедрение зависимости).
Поведение Say () заключается в том, что он вызывает метод Hello() в IHelloProvider.
Если вы оглянетесь назад на тест, мы создали реальный объект TestClass, поскольку мы действительно хотим протестировать код, который мы написали. Мы создали Mock IHelloProvider и сказали, что ожидаем, что у него будет вызван метод Hello(), и когда он это сделает, будет возвращена строка "Hello World".
Затем мы вызываем Say () и проверяем результаты, как и раньше.
Важно понимать, что нас не интересует поведение IHelloProvider, и поэтому мы можем высмеивать его, чтобы упростить тестирование. Нас интересует поведение TestClass, поэтому мы создали реальный TestClass, а не Mock, чтобы мы могли проверить его реальное поведение.
Я надеюсь, что это помогло прояснить, что происходит.
Moq не выполняет частичное моделирование и может только моделировать общедоступные виртуальные методы или интерфейсы. Когда вы создаете Mock, вы создаете совершенно новый T и удаляете все реализации любых общедоступных виртуальных методов.
Мое предложение состояло бы в том, чтобы сосредоточиться на тестировании публичной поверхности ваших объектов, а не их внутренних органов. Ваши внутренние органы получат покрытие. Просто убедитесь, что у вас есть четкое представление о том, каков ваш целевой класс, и не издевайтесь над этим (в большинстве случаев).
Частичное моделирование полезно только тогда, когда вы хотите проверить функциональность абстрактного класса с помощью имитированных реализаций абстрактных методов. Если вы этого не сделаете, вы, скорее всего, не увидите большой пользы от частичной насмешки.
Если это не абстрактный класс, вам нужно больше сосредоточиться на подходе, который предлагает Модан, а именно на том, что вы должны имитировать зависимости, а не сам целевой класс. Все это сводится к правилу: не издевайтесь над тем, что вы тестируете.