понедельник, 13 декабря 2010 г.

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

Теперь, предположим, что наши светлые головы посетила не менее светлая мысль, а не написать ли модульный тест замечательного класса 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();
    }
}

Теперь можно запустить этот код и в окне 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;
    }
}

Итак, сгенерированный класс явно реализует исходный интерфейс и содержит делегаты, типы которых соответствуют сигнатуре соответствующих методов. Поскольку наш метод не принимает никаких параметров и возвращает 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();
}

Запустив его мы, как и ожидается, увидим следующую строку: 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;
    }
}

Понятно, что если у нас есть возможность отрефакторить этот код и выделить интерфейс 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();
}

Обратите внимание на атрибут [HostTye(“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);
    }
}

Прежде чем приступить к написанию тестов, нам нужно сгенерировать молы для класса 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");
}

В данном тесте, вначале мы создаем заглушку для класса HttpWebResponse, расположенную в пространстве имен System.Net.Moles с именем MHttpWebRespoonse, и в качестве «тела» свойства ContentLength подсовываем нашу лямбду, возвращающую 5. Теперь мы «изменяем» метод GetResponse для всех экземпляров класса HttpWebRequest путем установки делегата GetResponse. После этого, мы можем спокойно вызвать метод RequestWebPage класса WebRequester и получить нормальный результат, даже без доступа к инету: Sync version. URL: http://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();
}

В тестовых целях я не хочу, чтобы метод завершал управление до завершения асинхронной операции. В данном коде мы не можем использовать 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);
}

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

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

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

11 комментариев:

  1. Спасибо за хорошую статью. В целом, Moles неплох. Главное, что позволяет избавиться от огромного количества wrapper`ов для всевозможных DateTime.Now и пр.. Единственное, что не понравилось, это автоматическая привязка среды выполнения тестов к MSTest.

    ОтветитьУдалить
  2. В документации по этой штуке есть примеры работы MS Moles с другими тестовыми фреймворками. Сам не проболвал, но, вроде бы, должно работать и с другими. Но я так понял, что Moles нельзя использовать с NUnit в Visual Studio, а только с командной строки NUnit (или любой другой билд тулзы). Что, в общем-то не есть гуд!

    ОтветитьУдалить
  3. То есть в связке решарпер + нюнит через тест раннер решарпера работать будет?

    ОтветитьУдалить
  4. Если судить из этого обсуждения (http://youtrack.jetbrains.net/issue/RSRP-260304), то решарпер с версии 6.1. можно будет использовать. Младшие версии работать не будут.

    ОтветитьУдалить
  5. Работаю с Moles уже несколько месяцев. Очень полезная фича, но есть несколько граблей, которые я так и не смог обойти:
    1. На каждую замоленную сборку при компиляции тестового проекта появляется варнинг, мол не найду файл SomeMyAssembly.Moles.dll
    Потом-то все компилируется и запускается нормально, но варнинг мешает.
    2. Не создается Moles для internal классов

    ОтветитьУдалить
  6. Этот комментарий был удален автором.

    ОтветитьУдалить
  7. Настораживает несколько вещей:
    1. Очень давно не обновляется, последняя версия датируется осенью 2010 года.
    2. Были анонсированы планы по выпуску новой версии в ноябре 2011 года, но так всё и осталось на уровне анонсов. Продукт производит впечатление заброшенного.
    3. Как следствие этого, наличие проблем вроде описанной здесь -> http://goo.gl/GUr3n и здесь -> http://goo.gl/rVLa4 настораживает. На моём рабочем ноутбуке я наткнулся на эту проблему, как решить - не понятно.

    ОтветитьУдалить
  8. Да, жаль, что развитие этой библиотеки остановилось. Хоть и не часто нужны возможности мокать, но тем не менее библиотека была весьма интересной.

    ОтветитьУдалить
  9. Внезапно:

    The Fakes Framework in Visual Studio 11 is the next generation of Moles & Stubs, and will eventually replace it.

    ОтветитьУдалить
  10. Да, я даже успел пощупать Moles в VS11. Единственное, их можно будет использовать только в prmium и ultimate.

    ОтветитьУдалить