Alexey Suvorov dev blog

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

Archive for the ‘tests’ Category

Mole IDisposable.Dispose in HttpWebResponse

5 комментариев

Тестируя код использующий HttpWebRequest внутри себя с помощью Moles столкнулся с таким сообщением: WebResponse.System.IDisposable.Dispose() was not moled.

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
            }
        }
    }
}

Written by alexeysuvorov

22.01.2011 at 3:14 пп

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

.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

.net Moles — часть 1

14 комментариев

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 &amp;&amp; bDelete &amp;&amp; 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 к проекту

Среда сформирует файл .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/

Written by alexeysuvorov

30.09.2010 at 5:24 пп

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