Руководство по эффективному тестированию решений Azure

Обновлено: Январь 2015 г.

Автор: Сурен Махираджу (Suren Machiraju)

Рецензенты: Хайме Альва Браво (Jaime Alva Bravo ) и Стив Уилкинс (Steve Wilkins)

После проектирования, написания кода и развертывания решения Microsoft Azure иногда можно обнаружить, что оно не работает. В данной статье предоставлены рекомендации по тому, как тестировать приложения Microsoft Azure на протяжении всего жизненного цикла разработки программного обеспечения. Область тестирования включает бизнес-логику и сквозные сценарии тестирования. В этой статье описывается следующее.

  • Разработка модульных тестов для компонентов бизнес-логики с устранением зависимостей от компонентов Microsoft Azure.

  • Проектирование интеграционных сквозных тестов.

  • Устранение издержек на установку, инициализацию, очистку и освобожде��ие ресурсов Microsoft Azure (например, очередей) для каждого теста.

  • Устранение создания дублирующихся инфраструктур для пространств имен ACS, очередей шины обслуживания и других ресурсов.

Кроме того, в этой статье представлен обзор различных методов и технологий тестирования приложений Microsoft Azure.

Новые изменения в этой статье:

  1. Мы используем Visual Studio 2013.

  2. Microsoft Fakes в Visual Studio 2013 заменяет Moles. Эта функция описана в статьеИзоляция кода в тесте с помощью Microsoft Fakes.

  3. В Visual Studio 2013 доступна новая версия покрытия кода, которая запускается непосредственно в обозревателе тестов. Ее описание см. в статье Использование покрытия кода для определения объема тестируемого кода.

  4. Группа Pex уже выпустила облегченную версию Pex — Code Digger. Описание см. в статье Microsoft Code Digger.

  5. Начиная с Visual Studio 2012, мы больше не рекомендуем проводить тестирование закрытых методов. Объяснение этому см. в статье Мой взгляд на модульное тестирование закрытых методов.

Есть два вида тестов:

  • Модульные тесты — узкие тесты, рассчитанные на конкретную функцию. Мы называем эти тесты тестируемым кодом или CUT. Необходимо удалить все зависимости, требуемые для CUT.

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

В целом эти тесты сосредоточены на создании и использовании тестовых дублей. Используются следующие типы тестовых дублей:

  • Имитации — смоделированные объекты, реализующие тот же интерфейс, что и объект, который они представляют. Они возвращают заранее заданные ответы. Имитация содержит набор методов-заглушек и служит заменой для того, что создается программным способом.

  • Заглушки моделируют поведение программных объектов.

  • Прокладки позволяют изолировать код из сборок, не являющихся частью решения. Они также отделяют компоненты решения друг от друга.

После выполнения эти тесты могут проверить состояние и поведение. Например, состояние включает результат после вызова метода и возврата конкретного значения. Пример поведения — вызов метода в определенном порядке или определенное число раз.

Одной из основных целей модульного тестирования является устранение зависимостей. Для платформы Azure эти зависимости таковы:

  • Очереди шины обслуживания.

  • Access Control Service.

  • Кэш.

  • Таблицы, BLOB-объекты и очереди Azure.

  • База данных SQL Azure.

  • Диск Azure (прежнее название — Облачный диск).

  • Другие веб-службы.

При построении тестов для приложения Azure мы заменим эти зависимости, чтобы сосредоточить тесты на логике.

Примеры для очередей шине обслуживания (включая средства и методики), которые мы обсудим в этой статье, также применимы к другим зависимостям.

Для реализации платформы тестирования приложений Microsoft Azure нужно следующее.

  • Платформа модульного тестирования для определения и запуска тестов.

  • Платформа имитации для изолирования зависимостей и построения узких модульных тестов.

  • Средства автоматической генерации модульных тестов для расширения покрытия кода.

  • Другие платформы, которые помогают строить тестируемую архитектуру за счет внедрения зависимостей и применения шаблона инверсии управления (IoC).

Visual Studio содержит программу командной строки MS Test, которая позволяет выполнять модульные тесты, созданные в Visual Studio. Visual Studio также включает набор шаблонов и элементов проекта для тестирования. Как правило, вы создаете тестовый проект, а затем добавляете классы (называемые тестовой арматурой) с атрибутом [TestClass]. Классы содержат методы с атрибутом [TestMethod]. Внутри MS Test различные окна в Visual Studio позволяют выполнять модульные тесты, определенные для проекта. Можно также просмотреть результаты проверки после их выполнения.

noteПримечание
Visual Studio 2013 Express, Professional и Test Professional не включают MS Test.

В MS Test модульные тесты используют схему AAA — организация, выполнение, оценка.

  • Организация — нужно создать все необходимые объекты, конфигурации и все другие необходимые условия и ресурсы для CUT.

  • Выполнение — проведение узкого теста кода.

  • Оценка — проверка получения ожидаемых результатов.

Библиотеки платформы MS Test включают вспомогательные классы PrivateObject и PrivateType. Эти классы используют отражение для упрощения вызова неоткрытых и статических членов из кода модульного теста.

Выпуски Visual Studio Premium и Ultimate включают расширенные средства модульного тестирования. Эти средства интегрируются с MS Test. Они также позволяют проанализировать объем кода, охваченного модульными тестами. Кроме того, они выделяют цветом исходный код для указания покрытия. Эта функция называется покрытием кода.

Цель модульного тестирования состоит в изолированном тестировании. Однако зачастую невозможно тестировать код в изоляции. Иногда код не предназначен для тестирования. Переписать код будет трудно, потому что он зависит от других библиотек, которые трудно изолировать. Например, это может быть код, который взаимодействует с внешней средой. Платформа имитации позволяет изолировать оба типа зависимостей.

Список таких платформ см. в разделе ссылок в конце этой статьи. Данная статья посвящена использованию Microsoft Fakes.

Microsoft Fakes помогает изолировать тестируемый код путем замены других частей приложения заглушками и прокладками. Эти небольшие блоки кода, которые находятся под контролем тестов. Изолируя код для тестирования, вы можете быть уверены, что если тест не пройден, причина в конкретном месте, а не где-то еще. Прокладки и заглушки также позволяют тестировать код, даже если остальные части приложения пока не работают.

Имитации делятся на два вида:

  • Заглушка заменяет класс небольшой подстановкой, которая реализует тот же интерфейс. Для использования заглушек необходимо проектировать приложение таким образом, чтобы каждый компонент зависел только от интерфейсов, а не от других компонентов. "Компонент" в данном случае — это класс или группа классов, разработанных и обновляемых совместно. Обычно они находятся в одной сборке.

  • Прокладка изменяет скомпилированный код приложения во время выполнения. Вместо вызова определенного метода приложение выполняет код прокладки, предоставленный тестом. Можно использовать прокладки для замены вызова сборок, которые вы не можете изменить, например сборок .NET.

Code Digger анализирует возможные пути выполнения кода .NET. Результатом будет таблица, в которой каждая строка отображает уникальный вариант поведения кода. Данная таблица позволяет понять поведение кода, а также выявить скрытые ошибки.

Чтобы проанализировать свой код в редакторе Visual Studio, используйте новый пункт контекстного меню — Создать таблицу ввода-вывода для вызова Code Digger. Code Digger вычисляет и отображает пары ввода-вывода. Code Digger систематически ищет ошибки, исключения и сбои проверок.

По умолчанию Code Digger работает только с общедоступным кодом .NET, находящимся в переносимых библиотеках классов. Далее в этой статье мы обсудим, как можно настроить Code Digger для изучения других проектов .NET.

Code Digger использует систему Pex и разрешитель ограничений Z3 от Microsoft Research для систематического анализа ветвления кода. Code Digger пытается создать набор тестов, который бы обеспечивал широкое покрытие кода.

Вы можете использовать Microsoft Unity для расширения внедрения зависимостей (DI) и инверсии управления (IoC). Платформа поддерживает перехват, внедрение конструкторов, свойств и вызовов методов. Microsoft Unity и аналогичные средства помогают строить тестируемые архитектуры, позволяющие внедрять зависимости на всех уровнях приложения. (Предполагается, что приложение было спроектировано и построено с внедрением зависимостей в расчете на одну из таких платформ.)

Эти платформы отлично подходят для написания тестируемого кода — а в конечном счете это дает хороший код. Тем не менее, они могут быть очень требовательными в отношении предварительных требований к проекте. Мы не обсуждаем контейнеры DI и IoC в этой статье.

В этом разделе рассматривается решение, содержащее веб-сайт, размещенный на веб-роли. Веб-сайт передает сообщения в очередь. затем рабочая роль обрабатывает сообщения из очереди. Нас интересует тестирование всех трех частей .

Предположим, у вас есть сайт для создания заказов и очередь шины обслуживания для обработки этих заказов. Веб-страницы напоминает структуру, показанную на рисунке 1:

Рис. 1

Рис. 1

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

noteПримечание
Класс MicrosoftAzureQueue является оболочкой для класса, использующего API .NET шины обслуживания (например, MessageSender и MessageReceiver) для взаимодействия с очередями шины обслуживания.

private IMicrosoftAzureQueue queue;
public OrderController()
{
    queue = new MicrosoftAzureQueue();
}
public OrderController(IMicrosoftAzureQueue queue)
{
    this.queue = queue;
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "OrderId,Description")] Order order)
{
    try
    {
        if (ModelState.IsValid)
        {
            string connectionString = CloudConfigurationManager.GetSetting("Microsoft.ServiceBus.ConnectionString");
            string queueName = "ProcessingQueue";
            queue.InitializeFromConnectionString(connectionString, queueName);
            queue.Send(order);
        }
        return View("OrderCreated");
    }
    catch (Exception ex)
    {
        Trace.TraceError(ex.Message);
        return View("Error");
    }            
}

Обратите внимание, что код получает настройки от CloudConfigurationManager и посылает сообщение, содержащее заказ, в очередь. Также обратите внимание, что действие Create использует следующие методы:

  • InitializeFromConnectionString (ConnectionString string, QueueName string)

  • Send (MicrosoftAzureQueue class)

Мы хотим построить обходной путь для этих методов с помощью подделок для управления их поведением и удаления зависимостей от реальной среды. Использование подделок устраняет необходимость выполнять тесты в эмуляторе Azure или вызывать очередь шины обслуживания. Мы выполняем действие Create контроллера для проверки того, что введенный заказ отправлен в очередь. Метод Send проверяет, что ИД и описание заказа переданы в действие. Затем он проверяет, что в результате отображается представление OrderCreated.

Создать модульный тест для такой реализации с помощью подделок просто. В тестовом проекте щелкните правой кнопкой мыши сборку, содержащую типы, которые требуется имитировать. Затем выберите пункт Добавить сборку имитаций.

В данном примере мы выберем Microsoft.Servicebus и используем команду Добавить сборку имитаций. XML-файл с именем "Microsoft.Servicebus.Fakes" добавляется в тестовый проект. Повторите действие для сборки Microsoft.WindowsAzure.Configuration.

При построении тестового проекта автоматически добавляются ссылки на сгенерированные автоматически версии-имитации этих сборок. В этом примере создаются сборки "Microsoft.ServiceBus.Fakes" и "Microsoft.WindowsAzure.Configuration".

Создайте метод модульного теста и примените атрибут [TestCategory("With fakes")]. В модульном тесте используйте прокладки, чтобы изолировать части приложения друг от друга.

Использование прокладок для изоляции приложения от других сборок при модульном тестировании

Прокладки — это одна из двух технологий, которые предоставляет Microsoft Fakes. Они позволяют легко изолировать тестируемые компоненты от среды. Прокладки перенаправляют вызовы отдельных методов в код, написанный в рамках теста. Многие методы возвращают разные результаты в зависимости от внешних условий. Однако прокладка находится под управлением теста и может возвращать согласованные результаты при каждом вызове. Это делает написание тестов намного проще. Используйте прокладки, чтобы изолировать код из сборок, не являющихся частью решения. Для изоляции компонентов решения друг от друга рекомендуется использовать заглушки.

Использование заглушек для изоляции частей приложения друг от друга при модульном тестировании

Заглушки — это одна из двух технологий, которые предоставляет платформа Microsoft Fakes. Они позволяют легко изолировать тестируемый компонент от других компонентов, которые он вызывает. Заглушка — это небольшой фрагмент кода, который занимает место другого компонента при тестировании. Преимущество использования заглушек заключается в том, что она возвращает последовательные результаты, что упрощает написание теста. Кроме того, вы можете запустить тесты даже в том случае, если компоненты еще не работают.

В нашем случае мы будем использовать прокладки для CloudConfigurationManager и BrokeredMessage из сборок Azure. Мы будем использовать заглушки для MicrosoftAzureQueue — это класс в нашем решении.

[TestMethod]
[TestCategory("With fakes")]
public void Test_Home_CreateOrder()
{
    // Shims can be used only in a ShimsContext
    using (ShimsContext.Create())
    {
        // Arrange
        // Use shim for CloudConfigurationManager.GetSetting
        Microsoft.WindowsAzure.Fakes.ShimCloudConfigurationManager.GetSettingString = (key) =>
        {
            return "mockedSettingValue";
        };
                
        // Create the fake queue:
        // In the completed application, queue would be a real one:
        bool wasCreateFromConnString = false;
        Order orderSent = null;
        IMicrosoftAzureQueue queue =
                new OrderWebRole.Queue.Fakes.StubIMicrosoftAzureQueue() // Generated by Fakes.
                {
                    // Define each method:
                    // Name is original name + parameter types:
                    InitializeFromConnectionStringStringString = (connectionString, queueName) => {
                        wasCreateFromConnString = true;
                    },
                    SendOrder = (order) => {
                    orderSent = order;
                    }
                };

        // Component under test
        OrderController controller = new OrderController(queue);

        // Act
        Order inputOrder = new Order()
        {
            OrderId = System.Guid.NewGuid(),
            Description = "A mock order"
        };
        ViewResult result = controller.Create(inputOrder) as ViewResult;

        //Assert
        Assert.IsTrue(wasCreateFromConnString);
        Assert.AreEqual("OrderCreated", result.ViewName);
        Assert.IsNotNull(orderSent);
        Assert.AreEqual(inputOrder.OrderId, orderSent.OrderId);
        Assert.AreEqual(inputOrder.Description, orderSent.Description);
    }
}

Веб-роль обеспечивает добавление заказов в очередь. Теперь посмотрим на то, как тестируется рабочая роль, обрабатывающая заказы с их извлечением из очереди шины обслуживания. Метод Run в рабочей роли периодически опрашивает очередь и обрабатывает полученные заказы.

private IMicrosoftAzureQueue queue;
public WorkerRole()
{
    queue = new MicrosoftAzureQueue();
}

public WorkerRole(IMicrosoftAzureQueue queue)
{
    this.queue = queue;
}
public override void Run()
{
    try
    {
        string connectionString = CloudConfigurationManager.GetSetting("Microsoft.ServiceBus.ConnectionString");
        string queueName = "ProcessingQueue";
               
        queue.InitializeFromConnectionString(connectionString, queueName);
            
        queue.CreateQueueIfNotExists();
              
        while (true)
        {
            Thread.Sleep(2000);
            //Retrieve order from Service Bus Queue  
            TryProcessOrder(queue);
        }
    }
    catch (Exception ex)
    {
        if (queue != null)
            queue.Close();
        System.Diagnostics.Trace.TraceError(ex.Message);
    }
}

Мы хотим протестировать его, чтобы убедиться, что он корректно извлекает сообщение. Ниже приводится полный модульный тест для метода Run рабочей роли.

[TestMethod]
public void Test_WorkerRole_Run()
{
    // Shims can be used only in a ShimsContext:
    using (ShimsContext.Create())
    {
        Microsoft.WindowsAzure.Fakes.ShimCloudConfigurationManager.GetSettingString = (key) =>
        {
            return "mockedSettingValue";
        };

        // Arrange 
        bool wasEnsureQueueExistsCalled = false;
        int numCallsToEnsureQueueExists = 0;

        // Create the fake queue:
        // In the completed application, queue would be a real one:
        bool wasConnectionClosedCalled = false;
        bool wasCreateFromConnString = false;
        bool wasReceiveCalled = false;
        int numCallsToReceive = 0;

        bool wasCompleteCalled = false;
        int numCallsToComplete = 0;
        IMicrosoftAzureQueue queue =
                new OrderWebRole.Queue.Fakes.StubIMicrosoftAzureQueue() // Generated by Fakes.
                {

                    // Define each method:
                    // Name is original name + parameter types:
                    InitializeFromConnectionStringStringString = (connectionString, queueName) =>
                    {
                        wasCreateFromConnString = true;
                    },
                    CreateQueueIfNotExists = () =>
                    {
                        wasEnsureQueueExistsCalled = true;
                        numCallsToEnsureQueueExists++;
                    },
                    Receive = () =>
                {
                    wasReceiveCalled = true;
                    if (numCallsToReceive >= 3) throw new Exception("Aborting Run");
                    numCallsToReceive++;
                    Order inputOrder = new Order()
                    {
                        OrderId = System.Guid.NewGuid(),
                        Description = "A mock order"
                    };
                    return new BrokeredMessage(inputOrder);
                },
                    Close = () =>
                    {
                        wasConnectionClosedCalled = true;
                    }

                };


        Microsoft.ServiceBus.Messaging.Fakes.ShimBrokeredMessage.AllInstances.Complete = (message) =>
        {
            wasCompleteCalled = true;
            numCallsToComplete++;
        };

        WorkerRole workerRole = new WorkerRole(queue);

        //Act
        workerRole.Run();

        //Assert
        Assert.IsTrue(wasCreateFromConnString);
        Assert.IsTrue(wasConnectionClosedCalled);
        Assert.IsTrue(wasEnsureQueueExistsCalled);
        Assert.IsTrue(wasReceiveCalled);
        Assert.AreEqual(1, numCallsToEnsureQueueExists);
        Assert.IsTrue(numCallsToReceive > 0);
        Assert.IsTrue(wasCompleteCalled);
        Assert.IsTrue(numCallsToComplete > 0);
        Assert.AreEqual(numCallsToReceive, numCallsToComplete);

    }
}

Необходимо задать делегат свойства AllInstances созданного типа-имитации. С помощью делегата любой созданный экземпляр имитации обходит все методы, для которых вы создали делегаты.

В этом примере мы хотим использовать метод Run исходного экземпляра, но при этом обойти методы экземпляра CreateQueue и TryProcessOrder. В коде мы создаем исключение, чтобы выйти из бесконечного цикла, который поддерживает метод Run, в заданное время.

Вы спросите, почему просто не использовать MessageSender, MessageReceiver и связанные с ними классы из SDK шина обслуживания напрямую вместо вставки вспомогательного типа? Чтобы полностью изолировать код таким образом, чтобы не вызывать реальную шину обслуживания, есть два варианта.

  • Создать имитации, наследующие от абстрактных классов пространства имен Microsoft.ServiceBus.

  • Позволить Fakes создать типы-имитации для них всех.

Проблема обоих подходов — сложность. Используя оба способа, в итоге вы придете к отражению таких классов, как TokenProvider и QueueClient. Отражение создает следующие проблемы:

  • Необходимо создавать производные этих абстрактных типов, которые предоставляют все необходимые перегрузки.

  • Необходимо предоставить внутренние типы, на которые опираются реальные версии этих классов.

  • Для внутренних типов необходимо воссоздать конструкторы или методы-фабрики таким способом, чтобы избежать зависимости от настоящей шины обслуживания.

Оптимальный вариант — включить собственный вспомогательный тип. Это все, что необходимо для имитации, обхода и изоляции кода от реальной шины обслуживания.

Чтобы проанализировать, что проверяют модульные тесты, мы можем изучить данные покрытия кода. Если запускать оба модульных теста в MS Test, мы увидим, что они пройдены. Мы также видим информацию о запуске в окне Обозреватель тестов.

Рис. 2

Рис. 2

Покрытие кода для тестов можно запускать из обозревателя тестов. Щелкните тест правой кнопкой мыши и выберите пункт Анализировать покрытие кода для выбранных тестов. Результаты отображаются в окне Результаты покрытия кода. Для активации сбора данных о покрытии кода настройте файл Local.testsettings в разделе Элементы решения. При открытии этого файла запускается редактор параметров теста.

Если ваше решение не содержит файл Local.testsettings, следует добавить его в решение при помощи следующей процедуры.

  1. Нажмите кнопку Добавить новый элемент.

  2. Выберите Тест, и затем щелкните Параметры теста.

    Рис. 3
    Рис. 3

  3. Щелкните вкладку Данные и диагностика и установите флажок справа от строки Покрытие кода.

  4. Далее нажмите кнопку НастроитьРисунок А.

  5. В окне Покрытие кода — сведения выберите все сборки, которые нужно тестировать, и нажмите кнопку ОК.

    Рис. 4
    Рис. 4

  6. Для закрытия редактора параметров теста нажмите кнопку Применить и закрыть.

  7. Перезапустите тесты, а затем нажмите кнопку Покрытие кода. Результаты покрытия кода должны выглядеть, как показано на рисунке 5.

    Рис. 5
    Рис. 5

  8. Щелкните значок Цвета отображения покрытия кода и перейдите к методу в сетке.

  9. Дважды щелкните метод. Его исходный код будет выделен цветом, показывающим области, которые были протестированы. Зеленый цвет указывает на то, что код был протестирован. Серый цвет указывает на то, что код был проверен частично либо не был проверен.

    Рис. 6
    Рис. 6

Ручное создании модульных тестов полезно, но Code Digger также может автоматически обновить модульные тесты. Он делает это, пытаясь использовать значения параметров, которые вы не учли. После установки Code Digger вы можете изучить метод, щелкнув его правой кнопкой мыши в редакторе кода и выбрав пункт Создать таблицу ввода-вывода.

Рис. 7

Рис. 7

Через некоторое время Code Digger закончит обработку и стабилизирует результаты. Например, на рисунке 8 показаны результаты работы Code Digger в методе TryProcessOrder рабочей роли. Обратите внимание, что Code Digger смог создать тест, вызывающий исключение. Code Digger также показывает созданные входные данные для создания исключения (значение null для параметра очереди), что еще более важно.

Рис. 8

Рис. 8

Показ: