Alexey Suvorov dev blog

Мой разработческий блог

Archive for Октябрь 2010

.net Moles — часть 2 (Stubs)

2 комментария

Изначально я не думал, что использование Moles вызовет столько вопросов о базовой функциональности, поэтому я хотел бы рассказать об основных возможностях. Во первых стоит отметить, что обычно писать классы моков самим не приходиться — их генерирует Moles framework анализируя сборку, классы из которой нужно заменить моками. Сборка, содержащая moles может быть созднана на основе любой .net сборки, будь то пользовательская или стандартная входящая в .net framework сборка.
В Moles существует 2 вида классов — S (Stub) и M (Moles). Разобраться в каком случае что использовать можно с помошью таблицы:
Возможность создать mock Stub Moles
Статические методы нет да
Sealed классы нет да
Internal типы да да
Private методы нет да
Статические конструкторы и финализаторы нет да
Абстрактные методы да нет

Stub

Stub классы по своей сути являются прокси классами, генерируемыми Moles framework для возможности переопределения виртуальных методов и задания поведения интефрейсам без необходимости реализации. Только Stub классы могут переопрделять виртуальные методы и методы интерфейсов. Далее я на примерах попробую рассказать что именно Moles framework генерирует и как этим пользоваться.

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 параметрами и/или результатом

Методы содержащие Generic параметры могут иметь моки. Для каждого конкретного набора generic классов может быть присовена своя реализация, т.е. мок на метод T GetInstance() может и должен быть заменён отдельно для каждого из интересующих нас T. Чтобы было более понятно посмотрим SIMyFavIoc, реализацию IMyFavIoc, которую нам сгенерировал Moles framework:

[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 и если делегат для виртуального метода не определён, то будет выброшено исключение, что на мой взгляд достаточно логично с точки зрения тестирования.

Поведение

Каждый Stub класс содержит свойство InstanceBehavior типа IBehavior. Методы данного объекта используются когда в тестах взываются методы, делегат для которых не задан. Если поведение не задано явно для данного экземпляра Stub класса, то используется значение поведения из статического свойства BehavedBehaviors.Current, проще говоря можно переопределить поведение по умолчанию для всех Stub классов присвоив значение этому свойству, напирмер в момент инициализации тестов или в любой другой. Теперь IBehavior в деталях:

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

Данные классы объявлены как private внутри класса BehavedBehaviors и доступны исключительно через его статические свойства, т.е. если нужно сменить поведение для конкретного объекта Stub типа на поведение DefaultValueStub, то:

SIDataAccessor acessorMock = new SIDataAccessor();
acessorMock.InstanceBehavior = BehavedBehaviors.DefaultValue;

Eсли же для всех сразу (кроме тех для кого поведение уже задано явно), то:

BehavedBehaviors.Current = BehavedBehaviors.DefaultValue;

Заключение

Stub классы — это «стандартный» подход к реализации моков, со всеми его достоинствами в плане скорости выполнения и ограничениями в плане невозможности подмены невиртуальных методов. Stub классы генерируются moles framework для всех интерфейсов и не sealed классов, правда надо отметить, что для не sealed классов не имеющих виртуальных или абстрактных методов сгенерированные Stub классы лишены смысла. И наконец Stub классы позволяют начать тестирование имея только интерфейсы и желание тестировать 🙂
Я сам не читаю некоторые статьи о прикладной разработке ПО, потому что они бывают слишком длинные или без примеров кода, поэтому разбор Moles классов я пожалуй оставлю для 3 части 🙂 в этой же статье постарался рассказать о Stub классах подробнее, чтобы ответить на вопросы, которые я увидел в комментариях к первой статье и при написании 3 части я обязательно учту вопросы, которые возможно появяться после прочтения этой статьи в комментариях.

Written by alexeysuvorov

07.10.2010 at 11:29 пп

Опубликовано в .net, moles, tests