Alexey Suvorov dev blog

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

Archive for Сентябрь 2010

.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 at 5:24 пп

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