Archive for the ‘moles’ Category
Mole IDisposable.Dispose in HttpWebResponse
Тестируя код использующий HttpWebRequest внутри себя с помощью Moles столкнулся с таким сообщением: WebResponse.System.IDisposable.Dispose() was not moled.
Сам код:
[TestMethod] [HostType("Moles")] public void TestWebRequestMole() { MHttpWebResponse resp1Mole = new MHttpWebResponse(); MHttpWebRequest.AllInstances.GetResponse = x => resp1Mole; using (var file1 = File.OpenRead("1.html")) { resp1Mole.GetResponseStream = () => file1; var request = HttpWebRequest.Create("http://valid.url"); using(var response = request.GetResponse()) { using (var reader = new StreamReader(response.GetResponseStream())) { var content = reader.ReadToEnd(); Assert.IsNotNull(content); //check content } } } }
HttpWebRequest при вызове GetResponse возвращает HttpWebResponse, который сам не имплементирует IDisposable напрямую, более того базовый WebResponse не содержит Mole свойства с именем Dispose потому что реализует IDisposble явно, зато у WebResponse есть другое свойство — SystemIDisposableDispose. Moles использует полную сигнатуру метода при реализации интерфейса явно. Код который работает:
[TestMethod] [HostType("Moles")] public void TestWebRequestMole() { MHttpWebResponse resp1Mole = new MHttpWebResponse(); MWebResponse.AllInstances.SystemIDisposableDispose = (x) => { }; MHttpWebRequest.AllInstances.GetResponse = x => resp1Mole; using (var file1 = File.OpenRead("1.html")) { resp1Mole.GetResponseStream = () => file1; var request = HttpWebRequest.Create("http://valid.url"); using(var response = request.GetResponse()) { using (var reader = new StreamReader(response.GetResponseStream())) { var content = reader.ReadToEnd(); Assert.IsNotNull(content); //check content } } } }
.net Moles — часть 2 (Stubs)
В Moles существует 2 вида классов — S (Stub) и M (Moles). Разобраться в каком случае что использовать можно с помошью таблицы:
Возможность создать mock | Stub | Moles |
Статические методы | нет | да |
Sealed классы | нет | да |
Internal типы | да | да |
Private методы | нет | да |
Статические конструкторы и финализаторы | нет | да |
Абстрактные методы | да | нет |
Stub
Stub для интерфейсов
public interface IMyInterface { int Method1(); void Method2(string x); }
добавив moles (да, я тоже удивлён как много значений у данного слова в рамках данной предтметной области, им обозначается практически всё, начиная от классов моков закакнчивая xml файлами и сгенерированными сборками) для сборки содержащей этот интерфейс и расковыряв её рефлектором внутри можно увидеть:
[DebuggerNonUserCode, DebuggerDisplay("Stub = IMyInterface"), StubClass(typeof(IMyInterface))] public class SIMyInterface : StubBase, IMyInterface { public MolesDelegates.Func Method1; public MolesDelegates.Action Method2String; int IMyInterface.Method1() { MolesDelegates.Func func = this.Method1; if (func != null) { return func(); } return this.InstanceBehavior.Result(this, "MolesDemo.IMyInterface.Method1"); } void IMyInterface.Method2(string x) { MolesDelegates.Action action = this.Method2String; if (action != null) { action(x); } else { this.InstanceBehavior.VoidResult(this, "MolesDemo.IMyInterface.Method2"); } } }
А именно: пара делегатов и реализация интерфейса, которая вызывает соответствующий методу делегат, если он предоставлен, если же нет, то подходящий к сигнатуре данного метода метод свойства InstanceBehavior. Чуть попозже я расскажу про то, что это, но на данном этапе ограничимся знанием, что по умолчанию все методы InstanceBehavior выбрасывают BehaviorNotImplementedException. У StubBase методов тоже нет, они есть у BehavedBase от которого StubBase отнаследован, но все они относятся к поведению, так что можно прейдти сразу к использованию. Собственно код:
//создаём объект мок класса `SIMyInterface interfaceMock = new SIMyInterface(); //присваеваем делегат, который будет вызван при вызове Method2 interfaceMock.Method2String = (x) => { Assert.AreEqual("val", x); }; //необязательно, но почему бы нет IMyInterface interfaceInstance = interfaceMock; //собственно тест interfaceInstance.Method2("val");
Данный пример лишён практического смысла, но я вполне могу представить что то вроде этого в бизнес логике:
public interface IDataAccessor { object GetData(); } public interface IMyFavIoc { T GetInstance(); } public class ReportGenerator { private readonly IDataAccessor _dataAccessor; public ReportGenerator(IMyFavIoc context) { _dataAccessor = context.GetInstance(); } public bool DoReport() { object data = _dataAccessor.GetData(); return data != null; } }
и тесты будут выглядеть как то так:
SIDataAccessor acessorMock = new SIDataAccessor(); acessorMock.GetData = () => { return new object(); }; SIMyFavIoc iocMoc = new SIMyFavIoc(); //о generic сразу после примера iocMoc.GetInstance<IDataAccessor>(() => { return acessorMock as IDataAccessor; }); IMyFavIoc ioc = iocMoc as IMyFavIoc; ReportGenerator generator = new ReportGenerator(ioc); Assert.IsTrue(generator.DoReport());
Надо заметить, что ни одни интерфейс ещё не имеет классов реализации, но мы уже что то тестируем и удивляем заказчика процентом покрытия.
Методы с generic параметрами и/или результатом
[StubClass(typeof(IMyFavIoc)), DebuggerDisplay("Stub = IMyFavIoc"), DebuggerNonUserCode] public class SIMyFavIoc : StubBase, IMyFavIoc { // Fields private StubDictionary getInstances; // Methods public void GetInstance(MolesDelegates.Func stub) { this.getInstances = StubDictionary.Concat(this.getInstances, stub); } T IMyFavIoc.GetInstance() { MolesDelegates.Func func; if (this.getInstances.TryGetValue(out func)) { return func(); } return this.InstanceBehavior.Result(this, "MolesDemo.IMyFavIoc.GetInstance"); } }
Внутри себя Moles использует словарь функций и находит подходящую для каждого набора generic параметров. С точки зрения использования это значит только, что лямбда выражение должно быть присвоено не делегату, а передано в соответствующую функцию, как это было сделано в примере с iocMoc.GetInstance<IDataAccessor>(() => { return acessorMock as IDataAccessor; }). Обычно не стоит беспокоиться о явном указании типа generic-а как это сделано у меня, потому что компилятор вычислит это за вас, но я оствил для наглядности.
Stub для не sealed классов
public class NotSealedClass { public int NotVirtualMethod() { return 0; } public virtual int VirtualMethod() { return 10; } }
будет сгенерирован класс (я удалил некоторые методы относящиеся к поведению для лучшей читаемости):
[DebuggerNonUserCode, StubClass(typeof(NotSealedClass)), DebuggerDisplay("Stub = NotSealedClass")] public class SNotSealedClass : NotSealedClass, IPartialStub, IStub, IBehaved { // Fields public MolesDelegates.Func VirtualMethod01; public override int VirtualMethod() { MolesDelegates.Func func = this.VirtualMethod01; if (func != null) { return func(); } if (this.CallBase) { return base.VirtualMethod(); } return this.InstanceBehavior.Result(this, "VirtualMethod"); } // Properties public bool CallBase { get;set; } }
Отличий от интерфейсов не много, внимания заслуживает свойство bool CallBase. С помощью него можно заставить вызывать версию метода находящуюся в NotSealedClass, но по умолчанию оно false и если делегат для виртуального метода не определён, то будет выброшено исключение, что на мой взгляд достаточно логично с точки зрения тестирования.
Поведение
public interface IBehavior { TResult Result(TBehaved target, string name) where TBehaved: IBehaved; bool TryGetValue(object name, out TValue value); void ValueAtEnterAndReturn(TBehaved target, string name, ref TValue value) where TBehaved: IBehaved; void ValueAtReturn(TBehaved target, string name, out TValue value) where TBehaved: IBehaved; void VoidResult(TBehaved target, string name) where TBehaved: IBehaved; }
Каждый из методов будет вызываться в зависимости от параметров которые принимает метод, для которого отсутствует ассоциированный пользователем делегат, например VoidResult для всех функций типа void Do(…params…), соответственно Result для всех функций с результатом.
Существует 3 реализации IBehavior входящие в Moles framework (почему эти классы названы *Stub не совсем ясно, я бы назвал их *Behavior):
- DefaultValueStub — возвращает default(T) для методов которые подразумевают возврат значения и не делает ничего для методов, которые возвращают void. Имя статического свойства: BehavedBehaviors.DefaultValue
- NotImplementedStub — выбрасывает исключение BehaviorNotImplementedException при попытке вызвать любой метод кроме TryGetValue, который возвращает false. Данное поведение является поведением по умолчанию. Имя статического свойства: BehavedBehaviors.NotImplemented
- CurrentProxyBehavior — возвращает результаты вызовов методов для BehavedBehaviors.Current, т.е.
public TResult Result(TBehaved target, string name) where TBehaved: IBehaved { return BehavedBehaviors.Current.Result(target, name); }
ну и далее в таком духе. Я пока не придумал где можно было бы его использовать. Имя статического свойства: BehavedBehaviors.CurrentProxy
SIDataAccessor acessorMock = new SIDataAccessor(); acessorMock.InstanceBehavior = BehavedBehaviors.DefaultValue;
Eсли же для всех сразу (кроме тех для кого поведение уже задано явно), то:
BehavedBehaviors.Current = BehavedBehaviors.DefaultValue;
Заключение
Я сам не читаю некоторые статьи о прикладной разработке ПО, потому что они бывают слишком длинные или без примеров кода, поэтому разбор Moles классов я пожалуй оставлю для 3 части 🙂 в этой же статье постарался рассказать о Stub классах подробнее, чтобы ответить на вопросы, которые я увидел в комментариях к первой статье и при написании 3 части я обязательно учту вопросы, которые возможно появяться после прочтения этой статьи в комментариях.
.net Moles — часть 1
Moles – о чём это
Moles – многозначное слово: родинка, крот, мол (дамба), грамм-молекула. Скорее всего в данном случае авторы имели в виду именно кротов, потому как крот и то, чем он занимается как нельзя лучше соответствует идее фреймворка. Подменять самые нижние инфраструктурные методы – это то, что выделяет moles. Итак, moles – легковесный фреймворк позволяющий заменить любой .net метод на делегат-заглушку, которая будет проверять входящие параметры и при необходимости возвращать фейковые данные. Что нибудь в последней фразе режет глаз? Да да, там написано любой метод.
Пример
Предположим есть некий класс, который содержит методы для генерации отчётов и сохранения их в файл. Логика работы класса со стороны пользователя выглядит так: генератору передаётся директория и шаблон имени файла, генератор обрабатывает шаблон, получает конечное имя файла и, если такой файл уже существует в данной директории, то он его удаляет и создаёт новый. Вот часть класса генератора.
public class ReportGenerator { public string GenerateReportToFile(string direcory, string fileNameTemplate, object data) { if (null == direcory) throw new ArgumentNullException("direcory"); if (null == fileNameTemplate) throw new ArgumentNullException("fileNameTemplate"); string fileName = ConvertPatternToFileName(fileNameTemplate, DateTime.Now); string fullPath = Path.Combine(direcory, fileName); if (File.Exists(fullPath)) { File.Delete(fullPath); } using (var sw = new StreamWriter(fullPath)) { sw.Write(data); } return fullPath; } private static string ConvertPatternToFileName(string template, DateTime extractDate) { string convertedFileName = template; string datePattern = "yyyyMMdd"; Regex regExp = new Regex(datePattern, RegexOptions.IgnoreCase); convertedFileName = regExp.Replace(convertedFileName, extractDate.ToString(datePattern)); return convertedFileName; } }
Юнит тесты в данном случае достаточно просты, вызвать генератор с нужными параметрами и убедиться, что создан файл с нужным содержимым, прочитав содержимое. Вопросы возникают тогда когда мы не хотим ничего никуда писать, потому что это долго, потому что потом это надо ещё и читать, а потом и удалять, а ещё тестовых данных может быть много и тогда всё вышеперечисленное множим на размер тестовых данных. Если быть до конца честным с собой, то никто не хочет писать никаих настоящих файлов, функции Write и Exists с большой долей вероятности сработают так как они и работали начиная с .net 1.0 и нет смысла их ещё раз тестировать. Всё, что нужно – это убедиться, что Exists, Delete и Write получили правильные параметры. Предвидя вопросы наподобии “а что если у нас нет доступа на запись и придёт злой exception?” хочу сразу отметить, что в данном случае надо разделить unit testing и integration testing. Unit тесты проверяют правильность работы логики программы и должны быть настолько независимы от окружения, насколько это позволяет логика. Задача Unit тестов проверить программу на машине разработчика, где права на доступ на запись в ту или иную директорию не имеют ничего общего с тем окружением в котором программа на самом деле будет работать. Но вернёмся к нашим баранам, точнее кротам. Не будем тянуть крота за усы вот код.
[assembly: MoledType(typeof(System.IO.File))] [assembly: MoledType(typeof(System.IO.TextWriter))] namespace TestProject1 { [TestClass] public class UnitTest1 { [TestMethod] [HostType("Moles")] public void CheckReportGenerator() { DateTime dateTime = DateTime.Today; string directoryName = "c:\\"; object data = new object(); string expectedFileName = string.Format("{3}{0}{1:00}{2:00}file.txt", dateTime.Year, dateTime.Month, dateTime.Day, directoryName); bool bExists = false, bDelete = false, bWrite = false; System.IO.Moles.MFile.ExistsString = (x) => { Assert.AreEqual(expectedFileName, x); bExists = true; return true; }; System.IO.Moles.MFile.DeleteString = (x) => { Assert.AreEqual(expectedFileName, x); bDelete = true; }; System.IO.Moles.MTextWriter.AllInstances.WriteObject = (t, a) => { Assert.AreEqual(data, a); bWrite = true; }; var repGenerator = new ReportGenerator(); repGenerator.GenerateReportToFile(directoryName, "yyyyMMddfile.txt", data); Assert.AreEqual(true, bExists && bDelete && bWrite); } } }
Давайте разбираться. В Moles различаются два вида типов. Stub types (перфикс S) для всех интрефейсов и не sealed классов, реализация очевидна – генерируется строго типизированный прокси при вызовах методов которого вызывается предоставленный делегат. И Moles types перфикс (M) – это тип позволяющий переопределить поведение невиртуальных методов, статических методов или методов sealed классов. О том как работает M внутри я расскажу в следующей части, если только трамвай меня не преедет, но если в двух словах, то Moles инструментирует процесс и используте профайлер для подмены тела метода на вызов заменяющего его делегата. Как пример System.IO.Moles.MFile позволяет подменить реализацию статических методов System.IO.File. Для каждого статического метода в Mole классе есть своё свойство типа Func<…> или Action<…>, например для File.Exists(string)
:
public class MFile { ... public static Func<bool,string> ExistsString { set { ... } } }
Добавление Moles в проект
Во время установки Moles интегрируется в Visual Studio, поэтому самый простой способ создать moles – это щёлкнуть правой кнопкой по сборке в References
Среда сформирует файл .moles который представляет из себя обычный xml файл содержащий имя сборки и Build Action для которого выставлен как Moles. До того как S и M классы будут доступны проект нужно скомпилировать, при этом в папке тестового проекта будет создана директория MolesAssemblies содержащая сгенерированные moles сборки и ссылки на эти сборки будут автоматически добавлены в тестовый проект. Так было не всегда, версия 0.93 например генерировала бинарные файлы и xml прямо в проекты, что было не очень приятно с точки зрения систем контроля версий.
Ограничения
На данный момент Moles ещё не достиг релизной версии, так что существует ряд ограничений, среди них невозможность на данный момент делать моки на методы принимающие больше 20 параметров, это техническое ограничение и оно может быть убрано в следующих версиях. Так же некоторые методы и классы mscorelib и методы содержащие точку в имени метода. Тестовые методы использующие Moles должны быть помечены аттрибутом [HostType(“Moles”)] и если используются M типы, то процесс должен быть должным образом инструментирован профайлером. Для mstest это происходит автоматически, но в мануале есть примеры с xUnit и MbUnit. Из практики могу добавить существующую в 0.94 версии проблему с длинными путями к проекту (более 255 символов), которую обещают исправить в ближайшей версии.
В сухом остатке
В любом случае Moles стоит 10-15 минут чтобы с ним разобраться и иметь его в виду если не на текущем, то на вновь начинающихся проектах, особенно когда выйдет релиз с нормальной интеграцией msbuild. В следующих статьях я планирую рассказать о behavior, свойстве AllInstances и внутреннем устройстве moles.
Ссылки
Официальная страница проекта http://research.microsoft.com/en-us/projects/pex/
Мануал по Moles http://research.microsoft.com/en-us/projects/pex/molesmanual.pdf
Форум на MSDN, очень живой и там всем отвечают http://social.msdn.microsoft.com/Forums/en-US/pex/threads/