Примеры использования Moq

в 17:02, , рубрики: .net, moq, тестирование, метки: , ,

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


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js