21 июня 2013

Знакомство с Moq

Moq - один из популярных фреймворков для создания mock-объектов. Его возможности широки настолько, насколько позволяет наследование в .NET. Под капотом - Castle, как и у Rhino.Mocks. В Moq нет разнообразия типов создаваемых объектов - они все mock-и. Есть только два режима работы mock-ов - это упрощает понимание и работу.
А вот что усложняет - так это то, что кроме собственно генерируемых объектов, Moq использует экземпляры класса Mock для настройки поведения объекта и проверок, было ли вызвано то, что ожидалось. Дальше по тексту Mock означает конкретный класс, а mock - сгенерированный экземпляр нужного нам типа. Для примеров я буду использовать вот такой интерфейс:
public interface IRepository
{
    string ReadData();

    string Storage { get; set; }

    void SetData(object o);

    event EventHandler OnReading;
}

Создание объектов

Объект можно создать несколькими способами.
Во-первых, можно вызвать
Mock<IRepository> mock = new Mock<IRepository>();
Этот конструктор создает Mock как для класса так и для интерфейса. Есть перегруженные варианты - можно указать режим, в котором должен работать mock, и параметра конструктора, если надо создать mock для класса, в котором нет конструктора по-умолчанию.
Режимы - это:
  • Loose - никогда не бросает исключений, пытается вернуть значения по-умолчанию
  • Strict - бросает исключения если вызван метод\свойство, поведение которого не задано предварительно.
Второй способ создания, это получить сразу mock-объект:
IRepository repo = Mock.Of<IRepository>();
Можно получить бесконечную коллекцию и выбрать столько экземпляров, сколько нужно:
IQueryable<IRepository> repos = Mocks.Of<IRepository>();
Чтобы получить возможность настроить поведение - для mock-объекта нужно получить соответствующий экземпляр Mock класса:
Mock<IRepository> mock = Mock.Get(repo);

Что может Mock

Этот класс дает возможность настроить поведение mock-объекта, выполнить проверки.
Вот его свойства:
  • Behavior - посмотреть в каком режиме был создан, Loose или Strict
  • CallBase - надо ли вызывать базовый класс, если не заданы ожидания
  • DefaultValue - что именно возвращать в режиме Loose. Можно возвращать null для reference-типов, а можно попытаться для них тоже создавать mock-и (для sealed классов mock создать не получится)
  • Object - экземпляр mock-объекта


Настройка поведения

Для настройки поведения есть куча методов:
  • Setup нужен для задания ожиданий для методов, возвращающих значения.
  • SetupGet - для вызова getter-ов свойств
  • SetupSet - для setter-ов свойств
  • SetupProperty - задает "property behavior" для свойства. Это значит, что свойство будет работать как хранилище: что ему присвоили - то и возвращает.
  • SetupAllProperties - то же что и SetupProperties - только для всех свойств скопом
Вся эта куча методов позволяет настраивать ожидания, используя цепочку вызовов (Fluent интерфейс): сначала задается член класса, потом - что он возвращает. Например:
mock.Setup(r => r.ReadData()).Returns(() => "some result");

mock.Setup(r => r.ReadData())
     .Callback(() => Console.WriteLine("Read call"))
     .Throws<ApplicationException>();

mock.Setup(r => r.ReadData()).Verifiable();

Довольно интуитивно: Returns задает что возвращать, Throws - что бросать, Callback - что вызывать.
Verifiable нужен для валидации. Все Verifiable ожидания будут проверены скопом при вызове Verify().

Есть еще SetReturnsDefault - он задает значение по умолчанию для некоего типа. Например String    это value тип. Соотвественно по умолчанию его значение - null. Но это можно изменить:
mock1.SetReturnsDefault("abc");
Теперь все значения строк по-умолчнию для mock1 будут "abc".

Если неизвестны заранее значения аргументов, передаваемых членам класса - это можно учесть с помощью класса It.
mock.Setup(r => r.SetData(It.IsInRange("a", "c", Range.Exclusive)));
mock.Setup(r => r.SetData(It.IsAny<int>()));
mock.Setup(r => r.SetData(
    It.IsRegex("^4[0-9]{12}(?:[0-9]{3})?$")));//VISA credit card
Можно вообще любой код проверки подключить:
Expression<Func<string, bool>> fridayPredicate =
    v => v == "HelloWorld" &&
    DateTime.Now.DayOfWeek == DayOfWeek.Friday;
mock.Setup(r => r.SetData(It.Is(fridayPredicate)));

Настройка валидации

Здесь Fluent интерфейса нет, а есть куча методов Verify, аналогичных Setup.
Можно проверить все заданные ожидания, помеченные как Verifiable - или просто все, вне зависимости от помеченности (методы Verify и VerifyAll).
Есть возможность проверить, был ли вызов конкретного члена класса. При проверке конкретного свойства\метода можно указать сообщение об ошибке и сколько раз должен был быть совершен вызов (куча вариантов - начиная от Never до диапазона).
var mock = new Mock<IRepository>();

mock.Verify();

mock.VerifyAll();

mock.Verify(r=>r.ReadData(), "Read Data was not called");
mock.Verify(r=>r.SetData(123));

mock.VerifyGet(r => r.Storage, Times.AtLeast(4));

mock.VerifySet(r => r.Storage = "", 
    Times.Between(1, 10, Range.Exclusive));

Для проверки параметров можно использовать уже знакомый класс It:
var mock = new Mock<IRepository>();

mock.Object.Storage = "b";

mock.VerifySet(r=>
    r.Storage = It.IsInRange("a", "c", Range.Exclusive));

События

Интересная возможность Moq - это работа с событиями. Для этого у класса Mock есть метод Raise. В качестве первого параметра передается делегат, который должен добавлять обработчик тому событию, которое нужно вызвать. Второй параметр - EventArgs для события. Хитрость в том, что Moq выполняет делегат в особом контексте, отслеживая какие вызовы каких членов класса были сделаны. Фреймворк ловит add метод события и понимает какое именно событие нужно вызвать. Метод может вызывать только virtual события (что логично - не virtual события могут быть инициированы только классом в котором они объявлены).
Выглядит вот так:
string called = "one";

var mock = new Mock<IRepository>();
IRepository repo = mock.Object;
repo.OnReading += (sender1, eventArgs) => { called += "called"; };

EventHandler sampleHandler = (sender, args) =>
    { throw new NotImplementedException(); };

mock.Raise(r => r.OnReading += sampleHandler, new EventArgs());

Assert.AreEqual(called, "onecalled");

Код присваиваемого делегата не важен - Moq перехватывает присваивание и не производит его фактически.

Заключение

Фреймворк довольно легкий и гибкий в использовании, интерфейс интуитивен и прост в освоении, нет излишней сложности, оставшейся из-за процесса развития библиотеки. Устаревшие конструкции помечены [Obsolete], код хорошо документирован.

Исходники выглядят неплохо, откровенной лапши не наблюдается, используются атрибуты [SuppressMessage] для управления анализом кода - почистить сообщения на методах, которые не могут быть сделаны иначе by design.

Я описал не все конструкции Moq, в нем есть еще чем воспользоваться, но, на мой взгляд, в сложных случаях возможностей может не хватить. Например: нет легкого пути проверить очередность вызовов (что вызовы совершаются в определенном порядке). Хотя необходимость подобных тестов сомнительна, уж слишком хрупкие они будут.

Moq понравился мне больше чем Rhino.Mocks, в обоих есть свои плюсы и минусы, но простота и организованность победили.