Moq – это простой и легковесный изоляционный фреймврк (Isolation Framework), который построен на основе анонимных методов и деревьев выражений. Для создания моков он использует кодогенерацию, поэтому позволяет «мокать» интерфейсы, виртуальные методы (и даже защищенные методы) и не позволяет «мокать» невиртуальные и статические методы.
ПРИМЕЧАНИЕ
На рынке существует лишь два фрейморка, позволяющих «мокать» все, что угодно. Это TypeMockIsolator и Microsoft Fakes, доступные в Visual Studio 2012 (ранее известные под названием Microsoft Moles). Эти фреймворки, в отличие от Moq, используют не кодогенерацию, а CLR Profiling API, что позволяет вклиниться практически в любой метод и создать моки/стабы даже для статических, невиртуальных или закрытых методов.
В Moq нет разделения между «стабами» (stubs) и «моками» (mocks) или, более формально, нет разделения на верификацию состояния и верификацию поведения. И хотя в большинстве случаев различия между стабами и моками не так уж и важны, а иногда одна и та же заглушка выполняет обе роли, мы будем рассматривать примеры от простых к сложным, поэтому вначале рассмотрим примеры проверки состояния, а уже потом перейдем к проверке поведения.
В качестве примера мы будем рассматривать набор юнит тестов для следующего интерфейса:
1. Стаб метода GetCurrentDirectory:
2. Стаб метода GetDirectoryByLoggerName, всегда возвращающий один и тот же результат:
3. Стаб метода GetDirrectoryByLoggerName, возвращающий результат в зависимости от аргумента:
4. Стаб свойства DefaultLogger:
5. Задание поведения нескольких методов одним выражением с помощью “moq functional specification” (появился в Moq v4):
6. Задание поведение нескольких методов с помощью вызова методов Setup («старый» v3 синтаксис):
ПРИМЕЧАНИЕ
Как я уже упоминал, в Moq нет разделения между моками и стабами, однако нам с вами будет значительно проще различать два синтаксиса инициализации заглушек. Так, «moq functional specification» синтаксис может использоваться только для тестирования состояния (т.е. для стабов) и не может применяться для задания поведения. Инициализация же заглушек методом Setup может быть, во-первых, более многословной, а во-вторых, при ее использовании не совсем понятно, собираемся ли мы проверять поведение или состояние.
Для тестирования поведения будет использоваться следующий класс и интерфейс:
1. Проверка вызова метода ILogWriter.Write объектом класса Logger (с любым аргументом):
2. Проверка вызова метода ILogWriter.Write с заданным аргументами:
3. Проверка того, что метод ILogWriter.Write вызвался в точности один раз (ни больше, ни меньше):
ПРИМЕЧАНИЕ
Существует множество вариантов проверки того, сколько раз вызвана зависимость. Для этого существуют различные методы класса Times: AtLeast(int), AtMost(int), Exactly, Between и другие.
4. Проверка поведения с помощью метода Verify (может быть удобной, когда нужно проверить несколько допущений):
5. Проверка нескольких вызовов с помощью метода Verify().
В некоторых случаях неудобно использовать несколько методов Verify для проверки нескольких вызовов. Вместо этого можно создать мок-объект и задать ожидаемое поведение с помощью методов Setup и проверять все эти допущения путем вызова одного метода Verify(). Такая техника может быть удобной для повторного использования мок-объектов, создаваемых в методе Setup теста.
Moq поддерживает две модели проверки поведения: строгую (strict) и свободную (loose). По умолчанию используется свободная модель проверок, которая заключается в том, что тестируемый класс (Class Under Test, CUT), во время выполнения действия (в секции Act) может вызывать какие угодно методы наших зависимостей и мы не обязаны указывать их все.
Так, в предыдущем примере метод logger.WriteLine вызывает два метода интерфейса ILogWriter: метод Write и SetLogger. При использовании MockBehavior.Strict метод Verify завершится неудачно, если мы не укажем явно, какие точно методы зависимости будут вызваны:
Класс MockRepository предоставляет еще один синтаксис для создания стабов и, что самое главное, позволяет хранить несколько мок-объектов и проверять более комплексное поведение путем вызова одного метода.
1. Использование MockRepository.Of для создания стабов.
Данный синтаксис аналогичен использованию Mock.Of, однако позволяет задавать поведение разных методов не через оператор &&, а путем использования нескольких методов Where:
2. Использование MockRepository для задания поведения нескольких мок-объектов.
Предположим, у нас есть более сложный класс SmartLogger, которому требуется две зависимости: ILogWriter и ILogMailer. Наш тестируемый класс при вызове его метода Write должен вызвать методы двух зависимостей:
В некоторых случаях бывает полезным получить сам мок-объект по интерфейсу (получить Mock<ISomething> по интерфейсу ISomething). Например, функциональный синтаксис инициализации заглушек возвращает не мок-объект, а сразу требуемый интерфейс. Это бывает удобным для тестирования пары простых методов, но неудобным, если понадобится еще и проверить поведение, или задать метод, возвращающий разные результаты для разных параметров. Так что иногда бывает удобно использовать LINQ-based синтаксис для одной части методов и использовать методы Setup – для другой:
Помимо этого Moq позволяет проверять поведение защищенных методов, тестировать события и содержит некоторые другие возможности.
Примеры на github
Моки и стабы
Microsoft Moles
ПРИМЕЧАНИЕ
На рынке существует лишь два фрейморка, позволяющих «мокать» все, что угодно. Это 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
