Moq – это простой и легковесный изоляционный фреймврк (Isolation Framework), который построен на основе анонимных методов и деревьев выражений. Для создания моков он использует кодогенерацию, поэтому позволяет «мокать» интерфейсы, виртуальные методы (и даже защищенные методы) и не позволяет «мокать» невиртуальные и статические методы.
ПРИМЕЧАНИЕ
На рынке существует лишь два фрейморка, позволяющих «мокать» все, что угодно. Это TypeMockIsolator и Microsoft Fakes, доступные в Visual Studio 2012 (ранее известные под названием Microsoft Moles). Эти фреймворки, в отличие от Moq, используют не кодогенерацию, а CLR Profiling API, что позволяет вклиниться практически в любой метод и создать моки/стабы даже для статических, невиртуальных или закрытых методов.
В Moq нет разделения между «стабами» (stubs) и «моками» (mocks) или, более формально, нет разделения на верификацию состояния и верификацию поведения. И хотя в большинстве случаев различия между стабами и моками не так уж и важны, а иногда одна и та же заглушка выполняет обе роли, мы будем рассматривать примеры от простых к сложным, поэтому вначале рассмотрим примеры проверки состояния, а уже потом перейдем к проверке поведения.
Проверка состояния (state verification)
В качестве примера мы будем рассматривать набор юнит тестов для следующего интерфейса:
public interface ILoggerDependency
{
string GetCurrentDirectory();
string GetDirectoryByLoggerName(string loggerName);
string DefaultLogger { get; }
}
1. Стаб метода GetCurrentDirectory:
// Mock.Of возвращает саму зависимость (прокси-объект), а не мок-объект.
// Следующий код означает, что при вызове GetCurrentDirectory()
// мы получим "D:\Temp"
ILoggerDependency loggerDependency =
Mock.Of<ILoggerDependency>(d => d.GetCurrentDirectory() == "D:\Temp");
var currentDirectory = loggerDependency.GetCurrentDirectory();
Assert.That(currentDirectory, Is.EqualTo("D:\Temp"));
2. Стаб метода GetDirectoryByLoggerName, всегда возвращающий один и тот же результат:
// Для любого аргумента метода GetDirectoryByLoggerName вернуть "C:\Foo".
ILoggerDependency loggerDependency = Mock.Of<ILoggerDependency>(
ld => ld.GetDirectoryByLoggerName(It.IsAny<string>()) == "C:\Foo");
string directory = loggerDependency.GetDirectoryByLoggerName("anything");
Assert.That(directory, Is.EqualTo("C:\Foo"));
3. Стаб метода GetDirrectoryByLoggerName, возвращающий результат в зависимости от аргумента:
// Инициализируем заглушку таким образом, чтобы возвращаемое значение
// метода GetDirrectoryByLoggerName зависело от аргумента метода.
// Код аналогичен заглушке вида:
// public string GetDirectoryByLoggername(string s) { return "C:\" + s; }
Mock<ILoggerDependency> stub = new Mock<ILoggerDependency>();
stub.Setup(ld => ld.GetDirectoryByLoggerName(It.IsAny<string>()))
.Returns<string>(name => "C:\" + name);
string loggerName = "SomeLogger";
ILoggerDependency logger = stub.Object;
string directory = logger.GetDirectoryByLoggerName(loggerName);
Assert.That(directory, Is.EqualTo("C:\" + loggerName));
4. Стаб свойства DefaultLogger:
// Свойство DefaultLogger нашей заглушки будет возвращать указанное значение
ILoggerDependency logger = Mock.Of<ILoggerDependency>(
d => d.DefaultLogger == "DefaultLogger");
string defaultLogger = logger.DefaultLogger;
Assert.That(defaultLogger, Is.EqualTo("DefaultLogger"));
5. Задание поведения нескольких методов одним выражением с помощью “moq functional specification” (появился в Moq v4):
// Объединяем заглушки разных методов с помощью логического «И»
ILoggerDependency logger =
Mock.Of<ILoggerDependency>(
d => d.GetCurrentDirectory() == "D:\Temp" &&
d.DefaultLogger == "DefaultLogger" &&
d.GetDirectoryByLoggerName(It.IsAny<string>()) == "C:\Temp");
Assert.That(logger.GetCurrentDirectory(), Is.EqualTo("D:\Temp"));
Assert.That(logger.DefaultLogger, Is.EqualTo("DefaultLogger"));
Assert.That(logger.GetDirectoryByLoggerName("CustomLogger"), Is.EqualTo("C:\Temp"));
6. Задание поведение нескольких методов с помощью вызова методов Setup («старый» v3 синтаксис):
var stub = new Mock<ILoggerDependency>();
stub.Setup(ld => ld.GetCurrentDirectory()).Returns("D:\Temp");
stub.Setup(ld => ld.GetDirectoryByLoggerName(It.IsAny<string>())).Returns("C:\Temp");
stub.SetupGet(ld => ld.DefaultLogger).Returns("DefaultLogger");
ILoggerDependency logger = stub.Object;
Assert.That(logger.GetCurrentDirectory(), Is.EqualTo("D:\Temp"));
Assert.That(logger.DefaultLogger, Is.EqualTo("DefaultLogger"));
Assert.That(logger.GetDirectoryByLoggerName("CustomLogger"), Is.EqualTo("C:\Temp"));
ПРИМЕЧАНИЕ
Как я уже упоминал, в Moq нет разделения между моками и стабами, однако нам с вами будет значительно проще различать два синтаксиса инициализации заглушек. Так, «moq functional specification» синтаксис может использоваться только для тестирования состояния (т.е. для стабов) и не может применяться для задания поведения. Инициализация же заглушек методом Setup может быть, во-первых, более многословной, а во-вторых, при ее использовании не совсем понятно, собираемся ли мы проверять поведение или состояние.
Проверка поведения (behavior verification)
Для тестирования поведения будет использоваться следующий класс и интерфейс:
public interface ILogWriter
{
string GetLogger();
void SetLogger(string logger);
void Write(string message);
}
public class Logger
{
private readonly ILogWriter _logWriter;
public Logger(ILogWriter logWriter)
{
_logWriter = logWriter;
}
public void WriteLine(string message)
{
_logWriter.Write(message);
}
}
1. Проверка вызова метода ILogWriter.Write объектом класса Logger (с любым аргументом):
var mock = new Mock<ILogWriter>();
var logger = new Logger(mock.Object);
logger.WriteLine("Hello, logger!");
// Проверяем, что вызвался метод Write нашего мока с любым аргументом
mock.Verify(lw => lw.Write(It.IsAny<string>()));
2. Проверка вызова метода ILogWriter.Write с заданным аргументами:
mock.Verify(lw => lw.Write("Hello, logger!"));
3. Проверка того, что метод ILogWriter.Write вызвался в точности один раз (ни больше, ни меньше):
mock.Verify(lw => lw.Write(It.IsAny<string>()),
Times.Once());
ПРИМЕЧАНИЕ
Существует множество вариантов проверки того, сколько раз вызвана зависимость. Для этого существуют различные методы класса Times: AtLeast(int), AtMost(int), Exactly, Between и другие.
4. Проверка поведения с помощью метода Verify (может быть удобной, когда нужно проверить несколько допущений):
var mock = new Mock<ILogWriter>();
mock.Setup(lw => lw.Write(It.IsAny<string>()));
var logger = new Logger(mock.Object);
logger.WriteLine("Hello, logger!");
// Мы не передаем методу Verify никаких дополнительных параметров.
// Это значит, что будут использоваться ожидания установленные
// с помощью mock.Setup
mock.Verify();
5. Проверка нескольких вызовов с помощью метода Verify().
В некоторых случаях неудобно использовать несколько методов Verify для проверки нескольких вызовов. Вместо этого можно создать мок-объект и задать ожидаемое поведение с помощью методов Setup и проверять все эти допущения путем вызова одного метода Verify(). Такая техника может быть удобной для повторного использования мок-объектов, создаваемых в методе Setup теста.
var mock = new Mock<ILogWriter>();
mock.Setup(lw => lw.Write(It.IsAny<string>()));
mock.Setup(lw => lw.SetLogger(It.IsAny<string>()));
var logger = new Logger(mock.Object);
logger.WriteLine("Hello, logger!");
mock.Verify();
Отступление от темы. Strict vs Loose модели
Moq поддерживает две модели проверки поведения: строгую (strict) и свободную (loose). По умолчанию используется свободная модель проверок, которая заключается в том, что тестируемый класс (Class Under Test, CUT), во время выполнения действия (в секции Act) может вызывать какие угодно методы наших зависимостей и мы не обязаны указывать их все.
Так, в предыдущем примере метод logger.WriteLine вызывает два метода интерфейса ILogWriter: метод Write и SetLogger. При использовании MockBehavior.Strict метод Verify завершится неудачно, если мы не укажем явно, какие точно методы зависимости будут вызваны:
var mock = new Mock<ILogWriter>(MockBehavior.Strict);
// Если закомментировать одну из следующих строк, то
// метод mock.Verify() завершится с исключением
mock.Setup(lw => lw.Write(It.IsAny<string>()));
mock.Setup(lw => lw.SetLogger(It.IsAny<string>()));
var logger = new Logger(mock.Object);
logger.WriteLine("Hello, logger!");
mock.Verify();
Использование MockRepository
Класс MockRepository предоставляет еще один синтаксис для создания стабов и, что самое главное, позволяет хранить несколько мок-объектов и проверять более комплексное поведение путем вызова одного метода.
1. Использование MockRepository.Of для создания стабов.
Данный синтаксис аналогичен использованию Mock.Of, однако позволяет задавать поведение разных методов не через оператор &&, а путем использования нескольких методов Where:
var repository = new MockRepository(MockBehavior.Default);
ILoggerDependency logger = repository.Of<ILoggerDependency>()
.Where(ld => ld.DefaultLogger == "DefaultLogger")
.Where(ld => ld.GetCurrentDirectory() == "D:\Temp")
.Where(ld => ld.GetDirectoryByLoggerName(It.IsAny<string>()) == "C:\Temp")
.First();
Assert.That(logger.GetCurrentDirectory(), Is.EqualTo("D:\Temp"));
Assert.That(logger.DefaultLogger, Is.EqualTo("DefaultLogger"));
Assert.That(logger.GetDirectoryByLoggerName("CustomLogger"), Is.EqualTo("C:\Temp"));
2. Использование MockRepository для задания поведения нескольких мок-объектов.
Предположим, у нас есть более сложный класс SmartLogger, которому требуется две зависимости: ILogWriter и ILogMailer. Наш тестируемый класс при вызове его метода Write должен вызвать методы двух зависимостей:
var repo = new MockRepository(MockBehavior.Default);
var logWriterMock = repo.Create<ILogWriter>();
logWriterMock.Setup(lw => lw.Write(It.IsAny<string>()));
var logMailerMock = repo.Create<ILogMailer>();
logMailerMock.Setup(lm => lm.Send(It.IsAny<MailMessage>()));
var smartLogger = new SmartLogger(logWriterMock.Object, logMailerMock.Object);
smartLogger.WriteLine("Hello, Logger");
repo.Verify();
Другие техники
В некоторых случаях бывает полезным получить сам мок-объект по интерфейсу (получить Mock<ISomething> по интерфейсу ISomething). Например, функциональный синтаксис инициализации заглушек возвращает не мок-объект, а сразу требуемый интерфейс. Это бывает удобным для тестирования пары простых методов, но неудобным, если понадобится еще и проверить поведение, или задать метод, возвращающий разные результаты для разных параметров. Так что иногда бывает удобно использовать LINQ-based синтаксис для одной части методов и использовать методы Setup – для другой:
ILoggerDependency logger = Mock.Of<ILoggerDependency>(
ld => ld.GetCurrentDirectory() == "D:\Temp"
&& ld.DefaultLogger == "DefaultLogger");
// Задаем более сложное поведение метода GetDirectoryByLoggerName
// для возвращения разных результатов, в зависимости от аргумента
Mock.Get(logger)
.Setup(ld => ld.GetDirectoryByLoggerName(It.IsAny<string>()))
.Returns<string>(loggerName => "C:\" + loggerName);
Assert.That(logger.GetCurrentDirectory(), Is.EqualTo("D:\Temp"));
Assert.That(logger.DefaultLogger, Is.EqualTo("DefaultLogger"));
Assert.That(logger.GetDirectoryByLoggerName("Foo"), Is.EqualTo("C:\Foo"));
Assert.That(logger.GetDirectoryByLoggerName("Boo"), Is.EqualTo("C:\Boo"));
Помимо этого Moq позволяет проверять поведение защищенных методов, тестировать события и содержит некоторые другие возможности.
Дополнительные ссылки
Примеры на github
Моки и стабы
Microsoft Moles
Автор: SergeyT