Alexey Suvorov dev blog

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

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

Среда сформирует файл .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 в 5:24 пп

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

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

Subscribe to comments with RSS.

  1. Поздравляю с первым постом! Надеюсь получится интересный и насыщенный блог.

    Dmitri

    02.10.2010 at 8:28 пп

  2. В первом примере (коде) кривая проверка параметров.

    if (null == direcory) throw new ArgumentNullException("direcory");
    if (null == direcory) throw new ArgumentNullException("fileNameTemplate");

    Наверное, второй раз проверяется fileNameTemplate?

    suglosta@habr

    03.10.2010 at 2:17 дп

  3. 6 строка первого листинга

    if (null == fileNameTemplate) throw new ArgumentNullException(«fileNameTemplate»);

    pavel

    03.10.2010 at 4:29 дп

  4. Да, спасибо, поправил.

    alexeysuvorov

    03.10.2010 at 7:54 дп

  5. А чем эта штука лучше чем стандартные Mock библиотеки — RhinoMocks например?

    Филипп

    03.10.2010 at 11:10 дп

    • Вся соль в невиртуальных методах, попробуйте реализовать тесты из статьи с помошью RhinoMocks 😉

      alexeysuvorov

      03.10.2010 at 9:57 пп

    • Для своего кода все отлично и RhinoMocks справляются с мокингом. Но например DateTime c чудным статическим свойством Now… Вот почему то не хочется вводить интерфейс IDateTime и везде его передавать в виде зависимости.

      Андрей

      04.10.2010 at 12:25 пп

  6. Спасибо за отличную статью.

    На сколько я знаю, во многих (если не во всех) Mock Framework’ах есть фичи по созданию в RunTim’е объекта по интерфейсу и задания его поведения. В Moles же, как я понял из документации, приходится создавать фэйковые классы имплементирующие необходимый интерфейс, что несколько осложняет написание тестов.
    Получается, что в некоторых кейсах удобней использовать например Moq, а там где он не может помочь (как в примере из статьи) — использовать Moles. Правильно я понимаю?

    И еще хочется добавить, что обычно я не писал юнит-тесты на классы, которые хоть как-то работают с файловой системой, базой данных (ну я думаю не я один), и наверное это потому, что не было такого фреймворка как Moles, который позволил бы переопределить поведение sealed/static-классов. Хочу как раз в скором времени опробовать его на практике.

    Pavel Belousov

    05.10.2010 at 12:34 пп

    • >Moles же, как я понял из документации, приходится создавать фэйковые классы имплементирующие необходимый интерфейс
      нет, в случае со stub — вы делаете то же самое что и во всех других фреймворках, т.е. например если у вас есть класс

      MyClassWithWirtualMethods {
       public virtual Do(string x){}
      }
      

      то молс сгенерирует

      SMyClassWithWirtualMethods{
       public  Action<string> DoString{set;}
      }
      

      т.е. вы сможете написать что то типа

      SMyClassWithWirtualMethods mock = new SMyClassWithWirtualMethods();
      mock.DoString = x=> { Assert.AreEqual("",x) };
      MyClassWithWirtualMethods instance = mock.Instance;
      instance.Do("");
      

      Т.е. молс не обязывает вас имплементировать интефейс, но предоставляет такую возможность заменить только те методы интерфейса которые вам нужны.

      Я постараюсь в ближайшее время написать ещё пост, чтобы разрешить неоднозначность, которая возникла вокруг Moles и его возможностей.

      alexeysuvorov

      05.10.2010 at 7:13 пп

      • Это я понял, а что в случае если у меня есть интерфейс, а ни одного класса еще нет (ну например я применяю TDD)? Moq например позволит создать Mock по интерфейсу и задать его поведение. А Moles, как я понимаю, сгенерит класс унаследованный от этого интерфейса и потом можно будет в этот класс передавать делегаты. Вот в данном кейсе мне кажется Moles несколько не удобен, т.к. зачем мне генерить какой-то еще класс если я могу и без него обойтись.

        Pavel Belousov

        06.10.2010 at 4:35 дп

  7. >Moq например позволит создать Mock по интерфейсу и задать его поведение. А Moles, как я понимаю, сгенерит класс унаследованный от этого интерфейса и потом можно будет в этот класс передавать делегаты.
    Абсолютно точно, только не понятно, чем же поведение «позволит создать Mock по интерфейсу» и «сгенерит класс унаследованный от этого интерфейса» отличаются с точки зрения пользователя? Если moq не угадывает реализацию, то я не вижу принципиальной разницы. Возможно лучше показать пример
    Сборка MolesDemo, в ней интерфейс.

    public interface IMyInterface {
      int Method1();
      void Method2(string x);
     }
    

    Проект с юнит тестами, в него добавляем референс на проект MolesDemo, потом правый клик на референсе «Generate moles», потом нужно сбилдить тесты, после этого в тест методе можно писать:

    SIMyInterface interfaceMock = new SIMyInterface();
    interfaceMock.Method2String = (x) => { Assert.AreEqual("val", x); };
    IMyInterface interfaceInstance = interfaceMock;
    interfaceInstance.Method2("val");
    

    Собственно больше ничего не потребуется, если в тесте нет вызова метода Method1, если он есть, то в поведении по умолчанию будет выброшено исключение

    alexeysuvorov

    06.10.2010 at 7:46 дп

    • Ну тут принципиальная разница-то как раз в том, что Moles что-то свое генерит, а Moq нет. С точки зрения девелопера понятно, что добавляется всего лишь одно дополнительное действие «Generate moles», но а нужно ли оно вообще? Ведь если сборка, для которой необходимо сгенерить moles, большая, то ведь и размер сгенеренных классов будет большой, что как мне кажется несколько неудобно, т.к. они увеличат размер репозитория, увеличат количество пересылаемых данных туда-обратно между клиентом и репозиторием. Я не хочу сказать, что это плохо или критично, просто я не вижу преимуществ Moles перед тем же Moq в данных кейсах 🙂

      Pavel Belousov

      07.10.2010 at 4:14 дп

      • Генерированные сборки вредно хранить в системе контроля версий, потому что они генерированные. Всё что нужно — это .moles файл, который представляет из себя обычно 3 строки xml и сгенерирует сборку на integration сервере или там где запускаются тесты. Сгенерированные классы храняться в сгенерированной сборке и только при очень большом желании могут быть включены в проект как исходный код, так что мне напирмер всё равно сколько классов он хранить в ещё одной временной сборке. Кстати молс предоставляет возможность не включать какие то классы или пространства имён в генерацию, так что теоретически в сборке может быть только 1 нужный класс, но это обычно бессмысленно. Достоинство молс не в Stub классах, его достоинство в Moles классах, а S они добавили просто чтобы не заставлять пользователя использовать 2 среды. Если нет необходимость делать моки на невиртуальные методы, то возможно молс будет слишком крут для такой задачи 🙂 Но возможность делать моки например на static factory методы подкупает.

        alexeysuvorov

        07.10.2010 at 7:15 дп


Оставьте комментарий