понедельник, 3 сентября 2012 г.

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

DISCLAIMER. Все приведенные здесь примеры можно найти на github.

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"));

5. Задание поведение нескольких методов с помощью вызова методов 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 метод logger.WriteLine завершится неудачно, если мы не укажем явно, какие точно методы зависимости будут вызваны:

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"));

Дополнительные ссылки

4 комментария:

  1. "При использовании MockBehavior.Strict метод Verify завершится неудачно"

    На самом деле, упадет исключение при вызове метода WriteLine :)

    ОтветитьУдалить
  2. @Костя: спасибо! Поправил текст.

    ОтветитьУдалить
  3. Имхо в статье не хватает следующего:

    Setup a property so that it will automatically start tracking its value (also known as Stub):
    // start "tracking" sets/gets to this property
    mock.SetupProperty(f => f.Name);

    Взято здесь:
    http://code.google.com/p/moq/wiki/QuickStart

    ОтветитьУдалить
  4. @Костик: спасибо! обязательно добавлю.

    ОтветитьУдалить