Microsoft Moles

    Moles – это легковесный тул от MS Research, который умеет автоматически генерировать заглушки для интерфейсов и виртуальных методов, а также для sealed классов, невиртуальных и статических методов (!), путем генерации кода, которому позднее можно будет подсунуть нужный делегат, вызываемый вместо определенного метода. Первый тип заглушек называется стабы (stubs), а второй – молы (moles). Именно эту штуку я использовал для тестирования асинхронных операций, о которых я рассказывал ранее, но давайте обо всем по порядку.

    Stubs



    Давайте рассмотрим такой пример. Предположим, что мы понимаем ценность модульных тестов, а также таких принципов, как Dependency Inversion, и других безумно полезных принципов и паттернов (может быть всех остальных принципов S.O.L.I.D., а возможно даже и F.I.R.S.T.). И дело даже не в том, что мы фанаты тестов или дядюшки Боба, а просто потому, что мы знаем, что высокая связность – это плохо. Поэтому мы стараемся в разумных пределах уменьшить зависимости путем выделения интерфейсов с последующим «инжектом» их в конструкторы классов или в методы, которым эти интерфейсы необходимы для выполнения своих задач.


    Итак, давайте предположим, что для некоторой абстракции в нашем коде мы выделили интерфейс IFoo и создали некий класс, этот интерфейс использующий.

    namespace PlayingWithMoles
    {
      // Интерфейс, выполняющий что-то ценное и полезное
      public interface IFoo
      {
        string SomeMethod();
      }
      // Какой-то класс, зависимый от интерфейса IFoo
      public class FooConsumer
      {
        // "Инжектим" интерфейс через конструктор класса
        public FooConsumer(IFoo foo)
        {
          this.foo = foo;
        }
        // Далее, где-то в коде мы используем интерфейс IFoo
        public void DoStuff()
        {
          var someResults = foo.SomeMethod();
          Console.WriteLine("Doing stuff. IFoo.SomeMethod results: {0}", someResults);
        }
        private readonly IFoo foo;
      }
    }


    * This source code was highlighted with Source Code Highlighter.


    Теперь, предположим, что наши светлые головы посетила не менее светлая мысль, а не написать ли модульный тест замечательного класса FooConsumer, с не менее замечательным методом DoStuff (понятно, что смысл в тестировании такого кода равен примерно нулю, но идею-то вы поняли). Итак, для создания экземпляра класса FooConsumer нужно найти реализацию интерфейса IFoo, которая может и есть в нашем коде, но не факт, что подходит для модульных тестов, поскольку нам может быть сложно (или вообще невозможно) ею воспользоваться, поскольку этот класс может зависеть от другого класса, который зависит от третьего и так далее. Конечно, такая мелочь нашего брата остановить не может, и если мы взялись за тестирование, то обязательно доведем дело до конца. Поэтому мы возьмем да реализуем интерфейс руками, благо тут метод всего один и сложности это не вызовет, в результате чего получим тест примерно такого вида:

    [TestClass()]
    public class FooConsumerTest
    {
      class FooTester : IFoo
      {
        public string SomeMethod()
        {
          return "Test string";
        }
      }
      [TestMethod]
      public void DoStuffTest()
      {
        IFoo foo = new FooTester();
        FooConsumer target = new FooConsumer(foo);
        target.DoStuff();
      }
    }


    * This source code was highlighted with Source Code Highlighter.


    Теперь можно запустить этот код и в окне Output увидеть: Doing stuff. IFoo.SomeMethod results: Hello, Custom stub. Да, это было не сложно, но мы-то с вами знаем, что проблемы реальных приложений кроются именно в деталях и то, что на практике мы сталкиваемся с куда более сложными интерфейсами, нежели этот и реализация каждого интерфейса руками вряд ли можно назвать самой интересной в мире работой.

    Итак, библиотека Moles. Создание заглушек этим тулом происходит следующим образом. В проекте с тестами, достаточно кликнуть правой кнопкой мыши на сборке с бизнес-логикой, которую вы хотите протестировать и выбрать пункт “Add Moles to Assembly”, после чего в проект будет добавлен файл “MyAssemblyName.moles”, и после компиляции этого проекта в списке подключенных сборок появится сборка MyAssemblyName.Moles, которая и будет содержать все сгенерированные заглушки. Затем этот инструмент будет автоматически отслеживать успешные билды сборки с бизнес-логикой и будет автоматически перегенерировать заглушки. Файл MyAssemblyName.moles представляет собой простой xml-файл в определенном формате, в котором задается имя сборки, для которой будут генерироваться заглушки, а также некоторые параметры их генерации (например, можно задать, для каких типов нужно генерировать заглушки, типы заглушек (stubs или moles) и многое другое).


    ПРИМЕЧАНИЕ
    Начиная с версии 0.94 изменилась схема moles файлов. До этого, вы могли отменить автоматическую компиляцию заглушек, задать Compilation = false в одной из секций конфигурационного файла, что приводило к тому, что сборка с заглушками не генерировалась, а вместо этого в текущий проект непосредственно добавлялся сгенерированный файл. Начиная с версии 0.94 такая возможность исчезла, но вы все равно можете посмотреть, как выглядит сгенерированный код, порывшись в подпапках вашего проекта. Так, например, текущая версия этого инструмента сохраняет сгенерированные файлы в следующем месте: MyTestProject\obj\Debug\Moles\sl\m.g.sl.


    Давайте посмотрим на упрощенный вариант кода, сгенерированного для нас этим инструментом:

    namespace PlayingWithMoles.Moles
    {
      public class SIFoo
       : Microsoft.Moles.Framework.Stubs.StubBase
       , IFoo
      {
        string IFoo.SomeMethod()
        {
          var sh = this.SomeMethod;
          if (sh != null)
            return sh.Invoke();
          else
          {
            // Здесь находятся всякие Behavior-ы, но нам это не интересно
          }
        }
        // Sets the stub of IFoo.SomeMethod
        public Func<string> SomeMethod;
      }
    }


    * This source code was highlighted with Source Code Highlighter.


    Итак, сгенерированный класс явно реализует исходный интерфейс и содержит делегаты, типы которых соответствуют сигнатуре соответствующих методов. Поскольку наш метод не принимает никаких параметров и возвращает string, заглушка содержит делегат типа Func<string> (т.е. делегат, возвращающий тип string и не принимающий никаких параметров). Далее, в методе SomeMethod, просто вызывается данный делегат (если этот делегат равен null, то по умолчанию будет сгенерировано исключение StubNotImplementedException, однако это поведение можно изменить). Обратите внимание, что имя класса заглушки следующее: SInterfaceOrClassName, и находится он в пространстве имен OriginalNamespace.Moles.

    Теперь давайте изменим наш исходный тест и воспользуемся сгенерированной для нас заглушкой:
    [TestMethod]
    public void DoStuffTestWithStubs()
    {
      var fooStub = new PlayingWithMoles.Moles.SIFoo();
      fooStub.SomeMethod = () => "Hello, Stub!";
      var target = new FooConsumer(fooStub);
      target.DoStuff();
    }


    * This source code was highlighted with Source Code Highlighter.


    Запустив его мы, как и ожидается, увидим следующую строку: Doing stuff. IFoo.SomeMethod results: Hello, Moles Stub!

    Еще раз напомню, что стабы (stubs) генерируются только для интерфейсов и виртуальных (не sealed) методов не sealed-классов. И здесь нет никакой магии, поскольку для этого генерируются классы-наследники или классы, реализующие ваши интерфейсы, а диспетчеризация вызовов происходит за счет старых добрых виртуальных вызовов. Да, эта штука весьма интересная и позволяет сэкономить немного времени, но в ней нет ничего такого удивительного, в отличие от … moles.

    Moles



    Молы (moles) – это второй тип заглушек, который предназначен для тех же целей, что и стабы, однако умеет работать со статическими или экземплярными, и невиртуальными методами. Давайте предположим, что наш класс FooConsumer завязан не на интерфейс IFoo, а на конкретный класс Foo, который не содержит виртуальных методов:

    namespace PlayingWithMoles
    {
      // Конкретный класс Foo, который выполняет не менее полезные
      // вещи, как и интерфейс IFoo
      public class Foo
      {
        public string SomeMethod()
        {
          return "Hello, from non-virtual method.";
        }
      }
      // Какой-то класс, зависимый от другого конкретного класса Foo
      public class FooConsumer
      {
        // Передаем экземпляр класса Foo через конструктор класса
        public FooConsumer(Foo foo)
        {
          this.foo = foo;
        }
        // Далее, где-то в коде мы используем экземпляр класс Foo
        public void DoStuff()
        {
          var someResults = foo.SomeMethod();
          Console.WriteLine("Doing stuff. Foo.SomeMethod results: {0}",
            someResults);
        }

        private readonly Foo foo;
      }
    }


    * This source code was highlighted with Source Code Highlighter.


    Понятно, что если у нас есть возможность отрефакторить этот код и выделить интерфейс IFoo, то так и стоит поступить, но иногда это может оказаться не целесообразным, а иногда и вообще невозможным. Часто бывает, что нам достается чужой код, развязывать клубки зависимостей в котором  нужно постепенно, а тесты нужны уже сейчас; да и не редки случаи, когда этот код просто невозможно изменить, поскольку вы его не контролируете; ведь это вполне может быть сторонняя библиотека или обращение к внешним ресурсам, тестирование доступности которых не входит в ваши намерения.

    Вот здесь как раз и поможет второй тип заглушек, которые могут генерироваться этим инструментом – молы. Молы генерируются аналогичным образом и располагаются в том же самом вложенном пространстве имен (OriginalNamespace.Moles), однако содержат префикс M вместо префикса S, однако вместо виртуальных методов и интерфейсов мы сможем задавать поведение невиртуальных статических методов. Для реализации этого поведения библиотека Moles использует CLR Profiler, в частности обрабатывает функцию обратно вызова ICorProfilerCallback::JITCompilationStarted в котором вместо оригинального метода «подсовывает» наш делегат. В результате, при вызове оригинального метода будет вызван предоставленный нами фрагмент кода.

    Таким образом, в нашем случае будет сгенерирован класс PlayingWithMoles.Moles.MFoo, и давайте посмотрим, как можно протестировать старый добрый (а главное полезный) метод DoStuff новой версии класса FooConsumer:

    [TestMethod]
    [HostType("Moles")]
    public void DoStuffTestWithMoles()
    {
      var fooMole = new PlayingWithMoles.Moles.MFoo();
      fooMole.SomeMethod = () => "Hello, Mole!";
      var target = new FooConsumer(fooMole);
      target.DoStuff();
    }


    * This source code was highlighted with Source Code Highlighter.


    Обратите внимание на атрибут [HostType(“Moles”)], без которого вся магия моулов работать перестанет, поскольку, как уже упоминалось ранее, для их работы используется «инструментирование» кода и CLR Profiler. Данный тест не сложнее предыдущего, в котором использовались стабы, а при запуске мы получим именно тот результат, который мы и ожидаем: Doing stuff. IFoo.SomeMethod results: Hello, Mole!

    Теперь давайте рассмотрим более интересный пример. В нескольких предыдущих постах я рассматривал разные способы танцев с бубнами вокруг асинхронных операций, и в качестве примеров я использовал асинхронное получение содержимого веб-страниц. Но поскольку мне иногда нужно было работать на компьютере без доступа к интернету, а каждый раз переписывать примеры не хотелось, как раз и возникла идея воспользоваться этим инструментом. Кроме того, хорошим правилом модульного тестирования является отвязка от внешних ресурсов (таких как файлы, сетевые соединения, база данных), поскольку в противном случае данные тесты уже не будет модульными, а будут интеграционными. Я не говорю, что интеграционные тесты – это плохо, нет, это хорошо, но в тестах модульных мы можем сосредоточиться на нашем коде, в то время как интеграционные тесты требуют наличие и нормальную работу этих внешних ресурсов.

    Давайте предположим, что у нас есть некоторый класс WebRequestor, который обращается к веб-странице и выводит длину полученного ответа:

    public class WebRequester
    {
      public void RequestWebPage(string url)
      {
        var request = WebRequest.Create(url);
        var response = request.GetResponse();
        Console.WriteLine("Sync version. URL: {0}, Response content length: {1}",
          url, response.ContentLength);
      }
    }


    * This source code was highlighted with Source Code Highlighter.


    Прежде чем приступить к написанию тестов, нам нужно сгенерировать молы для класса WebRequest. Для этого достаточно в тестовом проекте кликнуть правой кнопкой мыши по сборке System (поскольку именно в ней располагается этот класс), выбрать пункт меню “Add Moles Assembly”, затем перекомпилировать проект с тестами, в результате чего в проекте появится сборка System.Behaviors.dll, со всеми необходимыми стабами и молами оригинальной сборки System.dll.

    Теперь давайте посмотрим, как мы можем протестировать наш код:

    [TestMethod]
    [HostType("Moles")]
    public void RequestWebPageTest()
    {
      var mole = new System.Net.Moles.MHttpWebResponse();
      mole.ContentLengthGet = () => 5;
      // Мы можем возвращать нашу заглушку только для определенных URL-адресов.
      // Например, так:
      //System.Net.Moles.MHttpWebRequest.AllInstances.GetResponse = (r) =>
      //  {                              
      //    if (r.RequestUri == new Uri("http://rsdn.ru"))
      //      return mole;
      //    return r.GetResponse();
      //  };
      // А можем возвращать заглушку всегда
      System.Net.Moles.MHttpWebRequest.AllInstances.GetResponse = (r) => mole;
      WebRequester target = new WebRequester();
      target.RequestWebPage("http://rsdn.ru");
    }


    * This source code was highlighted with Source Code Highlighter.


    В данном тесте, вначале мы создаем заглушку для класса HttpWebResponse, расположенную в пространстве имен System.Net.Moles с именем MHttpWebRespoonse, и в качестве «тела» свойства ContentLength подсовываем нашу лямбду, возвращающую 5. Теперь мы «изменяем» метод GetResponse для всех экземпляров класса HttpWebRequest путем установки делегата GetResponse. После этого, мы можем спокойно вызвать метод RequestWebPage класса WebRequester и получить нормальный результат, даже без доступа к инету: Sync version. URL: rsdn.ru, Response content length: 5.

    Теперь давайте перейдем к асинхронной версии получении содержимого веб-страницы, методу RequestWebPageAsync класса WebRequester:

    public void RequestWebPageAsync(string url)
    {
      var waiter = new ManualResetEvent(false);
      var request = WebRequest.Create(url);
      request.BeginGetResponse(
        ar=>
          {
            var response = request.EndGetResponse(ar);
            Console.WriteLine("Async version. URL: {0}, Response content length: {1}",
              url, response.ContentLength);
            waiter.Set();
          }, null);
      
      waiter.WaitOne();
    }


    * This source code was highlighted with Source Code Highlighter.


    В тестовых целях я не хочу, чтобы метод завершал управление до завершения асинхронной операции. В данном коде мы не можем использовать AsyncResult.AsyncWaitHandle, возвращаемый методом BeginGetResponse, поскольку метод RequestWebPageAsync может продолжить выполнение до вызова метода request.EndGetResponse. В результате этого, смысла от такой «асинхронности» нет никакого, но это и не важно, поскольку здесь я хочу лишь показать механизм тестирования асинхронных операций и не более того. Но, думаю, что идея понятна.

    Итак, теперь тестовый метод:

    [TestMethod]
    [HostType("Moles")]
    public void RequestWebPageAsyncTest()
    {
      var mole = new System.Net.Moles.MHttpWebResponse();
      mole.ContentLengthGet = () => 5;
      // Делаем так, будто для выполнения нашей асинхронной операции
      // требуется некоторое время
      Action action = () => Thread.Sleep(500);
      
      // Это может пригодиться, если количество асинхронных операций,
      // которые вы хотите запустить асинхронно превышают минимальный
      // размер пула потоков по-умолчанию (по-умолчанию, размер пула потоков
      // равен количеству физических процессоров).
      ThreadPool.SetMinThreads(3, 3);
      System.Net.Moles.MHttpWebRequest.AllInstances.BeginGetResponseAsyncCallbackObject =
        (r, a, iar) => action.BeginInvoke(a, iar);
      System.Net.Moles.MHttpWebRequest.AllInstances.EndGetResponseIAsyncResult =
        (r, iar) => { action.EndInvoke(iar); return mole; };
      WebRequester target = new WebRequester();
      target.RequestWebPageAsync(http://rsdn.ru);
    }


    * This source code was highlighted with Source Code Highlighter.


    В нашем тестовом методе мы снова создаем заглушку MHttpWebResponse, экземпляр которой будет возвращать 5 при обращении к свойству ContentLength, и используем делегат, вызывающий Thread.Sleep(500), для имитации длительного выполнения операции. Запустив этот тест на выполнение, мы получим: Async version. URL: rsdn.ru, Response content length: 5. Чего и требовалось доказать!

    Вместо заключения



    Библиотека Moles – это весьма интересный инструмент, который поможет вам в генерации простых стабов для ваших интерфейсов, так и позволит отвязать ваш код от внешних ресурсов, статических или невиртуальных методов, создать заглушки для которых другим способом просто невозможно. Здесь я показал далеко не всю функциональность этого инструмента, но этого должно быть достаточно, чтобы понять, насколько он применим (или нет) для ваших конкретных задач.

    via Programming Stuff.
    Поделиться публикацией

    Похожие публикации

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 11

      +4
      Спасибо за замечательную статью!
        +1
        отличная статья, спасибо
          +1
          Интересный инструмент для автоматизации порой довольно рутинных операций. А еще вопрос, много «лишнего» кода генерирует данный иснтрумент Microsoft Moles?
            0
            Ну если я правильно понял, то код не лишним оказывается. Так как при релизной компиляции Moles не надо использовать (хотя это конечно дело вкуса). Поэтому и ни как не сказывается на объем кода и быстродействие.
              +1
              Молес нужен для тестов, и код генерится в тестах. Пофику релизных или не релизных. Объем кода приложения ессно не меняется.
              0
              Вы имеете возможность указать для каких именно типов генерировать Mole & Stub классы
              +1
              Отличная вещь этот Moles и автор кажется человек хороший. :-)
                0
                Уже давно используем Moles. Но вот при использовании постоянно есть ощущение неправильности происходящего. Оно то вроде классно — этот runtime instrumenting, но…

                В целом я бы порекомендовал использовать Moles в крайнем случае для Legacy-кода, который другим способом протестировать не вышло бы.

                Если проект пишется «с нуля» или разрабатываются какие-то новые слабо-зависимые компоненты или архитектура системы уже достаточно «слабо-связана», то имхо лучше использовать «обычные» библиотеки мокинга — RhinoMocks и т.д. Ну не покидает ощущение костыля при использовании Moles…
                  0
                  Да, нормальные стабы всегда предпочтительнее молов, но стабы не позволят отвязаться от внешнего environment-а, а вот с этим молы как раз справляются на ура.
                    +1
                    Меня скорее смущает runtime instrumenting — ощущение некоторой сюрреалистичности происходящего в тесте :) тестируешь var str = new String(); а String то уже может быть не тот String… :) (пример выдуманный и доведен до абсурда, никак не связанный с реальными кусками кода) Понимаешь что «так надо» и «без этого не протестируешь ибо слишком много зависимостей в старом коде», но это постоянное ощущение «подмены» мне не очень нравится.

                    Может это мое личное и пора сходить к психологу? :)
                  +1
                  Вы забыли добавить тег «Microsoft Moles».

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

                  Самое читаемое