Archive for Октябрь 2010
.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 части я обязательно учту вопросы, которые возможно появяться после прочтения этой статьи в комментариях.