DISCLAIMER: исходный код Verification Fakes можно найти на github, а скомпилированная версия доступна через nuget.org/packages/VerificationFakes.
В прошлой заметке мы рассмотрели примеры использования Microsoft Fakes для создания стабов – заглушек, возвращающих требуемые данные. Однако в некоторых случаях нам все же нужно проверить, что в определенных условиях тестируемый класс обращается к некоторой зависимости. Такой вид тестирования называется тестированием поведением и именно такой вид «подделок» каркасом Microsoft Fakes практически не поддерживается.
Все что можно сделать в Microsoft Fakes – это установить наблюдатель с помощью StubBase.InstanceObserver, но пользоваться таким подходом вручную довольно сложно.
Предположим, что нам у нас есть все тот же интерфейс ILogWriter, который затем передается через аргументы конструктора классу Logger и мы хотим убедиться в том, что в нужные моменты времени Logger таки вызывает нужные методы своей зависимости.
public interface ILogWriter
{
void Write(string message);
void Write(int value);
}
public class Logger
{
private readonly ILogWriter _logWriter;
public Logger(ILogWriter logWriter)
{
_logWriter = logWriter;
}
public void Write(string message)
{
_logWriter.Write(message);
}
public void Write(int value)
{
_logWriter.Write(value);
}
}
Так, имея заглушку интерфейса ILogWriter, сгенерированную Microsoft Fakes, мы можем написать тест, который будет проверять, что Logger при вызове метода Write вызывает соответствующий метод своей зависимости.
Как уже говорилось выше, для этого вначале нужно реализовать интерфейс IStubObserver и передать его сгенерированному стабу:
class CustomObserver : IStubObserver
{
public string CalledMethodName;
public object CalledMethodArgument;
public void Enter(Type stubbedType, Delegate stubCall, object arg1)
{
CalledMethodName = stubCall.Method.Name;
CalledMethodArgument = arg1;
}
// Остальные перегрузки методы Enter опущены
}
[Test]
public void Logger_Write_Calls_For_Enter_Method()
{
// Arrange
var stub = new StubILogWriter();
var customObserver = new CustomObserver();
// Providing custom observer to the stub
stub.InstanceObserver = customObserver;
var logger = new Logger(stub);
// Act
logger.Write("Message");
// Assert
Assert.That(customObserver.CalledMethodName,
Is.StringContaining("ILogWriter.Write"));
Assert.That(customObserver.CalledMethodArgument, Is.EqualTo("Message"));
}
Очевидно, что подобный код ничем не отличается от создания моков вручную. Но поскольку точка расширения в Microsoft Fakes все же существует, я написал легковесную оболочку вокруг этой библиотеки, которая прячет всю существующую логику и предоставляет возможности по тестированию поведения элегантным образом.
Тестирование поведения с помощью Verification Fakes
Verification Fakes – это легковесная оболочка над Microsoft Fakes, которая предоставляет интерфейс для тестирования поведения, аналогичный библиотеке Moq. Поэтому если у вас есть опыт использования Moq, то примеры Verification Fakes будут очень знакомыми.
Тогда, если мы сгенерируем фейки для этих интерфейсов, то сможем тестировать поведение с помощью Verification Fakes. Для этого из сгенерированного стаба нужно получить экземпляр Mock<T> с помощью методов расширения AsMock или AsStrictMock:
1. Метод Logger.Write(message) вызывает ILogWriter.Write с любым аргументом:
var stub = new StubILogWriter();
Mock<ILogWriter> mock = stub.AsMock();
var logger = new Logger(stub);
// Вызываем метод тестируемого класса
logger.Write("Hello, logger!");
// Проверяем, что был вызван метод Write с любым аргументом
mock.Verify(lw => lw.Write(It.IsAny<string>()));
Теперь, если мы закомментируем строку с вызовом метода logger.Write, то метод mock.Verify сгенерирует исключение VerificationException с соответствующим сообщением об ошибке:
VerificationFakes.VerificationException: Expected invocation on the mock 1 times, but was 0 times.
Expected call: lw => lw.Write(It.IsAny<string>())
Actual calls:
No invocations performed.
2. Метод LogWriter.Write вызывается с заданным аргументом:
// Вызываем метод тестируемого класса
logger.Write("Hello, logger!");
// Проверяем, что вызван метод с конкретными аргументами
mock.Verify(lw => lw.Write("Hello, logger!"));
В этом случае, если будет вызван метод Write с другим аргументом мы получим следующую ошибку:
Expected invocation on the mock 1 times, but was 0 times.
Expected call: lw => lw.Write("Hello, logger!")
Actual calls:
VerificationFakes.Samples.ILogWriter.Write("Some other message")
3. Метод LogWriter.Write вызывается в точности один раз:
// Вызываем метод тестируемого класса
logger.Write("Hello, logger!");
// Проверяем, что метод вызван в точности один раз
mock.Verify(lw => lw.Write(It.IsAny<string>()),
Times.Once());
ПРИМЕЧАНИЕ
Аналогично библиотеке Moq, Verification Fakes поддерживает возможность указать, сколько раз был вызван метод. Для этого служат фабричные методы класса Times: AtLeast(int), AtMost(int), Exactly и Between.
4. Метод LogWriter.Write(int) не вызывался при вызове Logger.Write(string):
// Вызываем метод тестируемого класса
logger.Write("Hello, logger");
// Проверяем, что метод Write(int) не был вызван
mock.Verify(lw => lw.Write(It.IsAny<int>()), Times.Never());
5. Метод LogWriter.Write(int) вызван с аргументом в указанном диапазоне:
// Вызываем метод тестируемого класса
logger.Write(42);
// Проверяем, что аргумент метод Write(int) находится в указанном диапазоне
mock.Verify(lw => lw.Write(It.IsInRange(40, 50)));
6. Использование методов Setup и Verify для указания нескольких ожидаемых вызовов:
var stub = new StubILogWriter();
var mock = stub.AsMock();
mock.Setup(lw => lw.Write(It.IsAny<string>()));
mock.Setup(lw => lw.Write(It.IsAny<int>()));
var logger = new Logger(mock.Object);
// Вызываем два метода тестируемого класса
logger.Write("Hello, logger!");
logger.Write(42);
// Проверяем, что были вызваны все методы,
// указанные путем вызова метода Setup
mock.Verify();
7. Пример использование строгого мока:
var stub = new StubILogWriter();
var mock = stub.AsStrictMock();
mock.Setup(lw => lw.Write("Foo"));
// Вызываем метод тестируемого класса
var logger = new Logger(mock.Object);
logger.Write("Foo");
В этом случае, при попытке вызова любого метода LogWriter помимо метода Write с указанным аргументом, мы получим VerificationException. Так, в случае вызова метода logger.Write(42) мы получим следующее сообщение:
Following invocations failed with mock behavior Strict:
VerificationFakes.Samples.ILogWriter.Write(42)
Expected invocations:
lw => lw.Write("Foo"), 1 times.
Заключение
Я честно признаюсь, что отношусь к тестированию поведения с осторожностью. При интенсивном использовании возникает опасность создания множества хрупких тестов, которые проверяют не абстрактное поведение, а завязаны на внутреннюю реализацию тестируемого класса.
Тем не менее, иногда такие тесты вполне полезны, особенно если они не завязаны на точное значение аргументов и проверяют действительно ключевое поведение тестируемых классов.
Для меня же Verification Fakes был весьма интересным домашним проектом, и, возможно, он будет интересен тем двоим программистам, которые используют Microsoft Fakes в своих проектахJ
Поправьте опечаточки, пожалуйста:
ОтветитьУдалитьВ заголовке статьи: Тестирование поведениЯ
Так, имЕя заглушку интерфейса ILogWriter
Спасибо, поправил.
УдалитьСпасибо за статью
ОтветитьУдалитьЯ использую способ попроще, кстати, он описан у Вас в предыдущей статье
Цитирую Ваш код:
var stub = new StubILoggerDependency();
ILoggerDependency loggerDependency = stub;
bool eventOccurred = false;
loggerDependency.RollingRequired += (sender, args) => eventOccurred = true;
// Вызываем событие вручную
stub.RollingRequiredEvent(this, EventArgs.Empty);
Assert.IsTrue(eventOccurred, "Event should be raised!");
То есть Вы объявляете переменную eventOccurred, а в конце проверяете чтобы она была true
Такой же подход можно использовать и в замоканых методах стаба. И еще внутри проверять соответствие переданных параметров ожидаемым (sender, args в Вашем случае)
А еще вместо булевой переменной можно использовать счетчик вызовов или единый логгер последовательности вызовов. Тут уж как фантазия позволит :)
ИМХО это гибче чем IStubObserver
Спасибо за дополнение!
УдалитьЯ и правда когда-то использовал аналогичный подход, но мне он показался все же более "императивным" по сравнению с "декларативным" подходом на основе велосипеда.
Хотя внедрение подобного велосипеда само по себе дело довольно непростое.