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, в обоих есть свои плюсы и минусы, но простота и организованность победили.