Автоматизированное тестирование

196

Инфраструктура ASP.NET MVC Framework спроектирована для максимального облегчения подготовки автоматизированных тестов и использования таких методологий, как разработка через тестирование (test-driven development - TDD), которая будет описана далее. ASP.NET MVC предоставляет идеальную платформу для автоматизированного тестирования, а в Visual Studio имеется ряд удобных средств тестирования. Все вместе это превращает разработку и прогон тестов в простую и легко решаемую задачу.

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

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

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

Модульное тестирование

В мире .NET для хранения тестовых оснасток создается отдельный тестовый проект в решении Visual Studio. Этот проект будет создаваться при первом добавлении модульного теста или же устанавливаться автоматически в случае использования шаблона проекта MVC. Тестовая оснастка - это класс C#, который определяет набор тестовых методов, по одному для каждого поведения, нуждающегося в проверке. Тестовый проект может содержать множество классов тестовых оснасток.

Для начала создадим класс из воображаемого приложения, как показано в примере ниже. Этот класс имеет имя AdminController и определяет метод ChangeLoginName, который позволяет воображаемым пользователям изменять свои пароли. Для этой демонстрации классы созданы в новом проекте Visual Studio под названием TestingDemo:

using System.Web.Mvc;

namespace TestingDemo {

    public class AdminController : Controller {
        private IUserRepository repository;

        public AdminController(IUserRepository repo) {
            repository = repo;
        }

        public ActionResult ChangeLoginName(string oldName, string newName) {
            User user = repository.FetchByLoginName(oldName);
            user.LoginName = newName;
            repository.SubmitChanges();
            // Отобразить некоторое представление для отображения результатов
            return View();
        }
    }
}

Контроллер опирается на ряд классов модели и интерфейс, код которых приведен в примере ниже. Эти классы не относятся к какому-то реальному проекту; они намеренно упрощены для облегчения демонстрации тестирования. Например, здесь не предполагается, что вы будете создавать классы, представляющие пользователей, которые имеют всего лишь одно строковое свойство.

namespace TestingDemo {

    public class User {
        public string LoginName { get; set; }
    }

    public interface IUserRepository {
        void Add(User newUser);
        User FetchByLoginName(string loginName);
        void SubmitChanges();
    }

    public class DefaultUserRepository : IUserRepository {

        public void Add(User newUser) {
            // необходимо реализовать
        }

        public User FetchByLoginName(string loginName) {
            return new User() { LoginName = loginName };
        }

        public void SubmitChanges() {
            // необходимо реализовать
        }
    }
}

Класс User представляет пользователя внутри приложения. Пользователи создаются, управляются и сохраняются в хранилище, функциональность которого определена интерфейсом IUserRepository, и неполная реализация этого интерфейса выполнена в классе DefaultUserRepository.

Цель, которая преследуется в этом разделе, состоит в написании модульного теста для функциональности, предоставляемой методом ChangeLoginName() класса AdminController:

using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace TestingDemo.Tests {

    [TestClass]
    public class AdminControllerTests {

        [TestMethod]
        public void CanChangeLoginName() {

            // Организация (настройка сценария)
            User user = new User() { LoginName = "Bob" };
            FakeRepository repositoryParam = new FakeRepository();
            repositoryParam.Add(user);
            AdminController target = new AdminController(repositoryParam);
            string oldLoginParam = user.LoginName;
            string newLoginParam = "Joe";

            // Действие (попытка выполнения операции)
            target.ChangeLoginName(oldLoginParam, newLoginParam);

            // Утверждение (проверка результатов)
            Assert.AreEqual(newLoginParam, user.LoginName);
            Assert.IsTrue(repositoryParam.DidSubmitChanges);
        }
    }

    class FakeRepository : IUserRepository {
        public List<User> Users = new List<User>();
        public bool DidSubmitChanges = false;

        public void Add(User user) {
            Users.Add(user);
        }

        public User FetchByLoginName(string loginName) {
            return Users.First(m => m.LoginName == loginName);
        }

        public void SubmitChanges() {
            DidSubmitChanges = true;
        }
    }
}

Тестовой оснасткой является метод CanChangeLoginName(). Обратите внимание, что этот метод декорирован атрибутом TestMethod, а класс, которому он принадлежит - AdminControllerTests - атрибутом TestClass. Именно так Visual Studio находит тестовую оснастку.

Структура метода CanChangeLoginName() следует шаблону, который известен под названием организация/действие/утверждение (arrange/act/assert - A/A/A). Организация относится к настройке условий теста, действие - к выполнению теста, а утверждение - к проверке того, что результат оказался тем, который требовался. Соблюдение согласованности структуры методов, реализующих модульные тесты, способствует их лучшей читабельности - характеристика, которую вы оцените по достоинству, если проект будет содержать многие сотни модульных тестов.

Тестовая оснастка из примера использует специфичную для теста фиктивную реализацию интерфейса IUserRepository, чтобы эмулировать конкретное условие - в данном случае ситуацию, когда единственный объект участника аукциона, Bob, присутствует в хранилище. Создание фиктивного хранилища и объекта User осуществляется в разделе организации теста.

Затем вызывается тестируемый метод - AdminController.ChangeLoginName. Это раздел действия теста. И, наконец, результат проверяется с помощью пары вызовов методов класса Assert (это раздел утверждения теста). Класс Assert предоставляется тестовым набором Visual Studio (пространство имен Microsoft.VisualStudio.TestTools.UnitTesting) и позволяет проверять специфичные исходы.

Запуск теста производится через меню Test среды Visual Studio. По мере выполнения тестов мы получаем визуальный отклик, как показано на рисунке ниже:

Визуальная обратная связь при выполнении модульных тестов

Если тестовая оснастка запускается без генерации каких-либо необработанных исключений и все операторы вызова методов класса Assert выполняются без проблем, окно Test Explorer (Проводник тестов) отображает зеленый маркер. В противном случае мы увидим красный маркер и подробности о возникшей проблеме.

Как видите, применение DI (которое мы обсуждали в предыдущей статье) облегчило модульное тестирование. Мы смогли создать фиктивную реализацию хранилища и внедрить ее в контроллер для создания специфичного сценария. Это одна из причин популярности шаблона проектирования DI.

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

Использование разработки через тестирование и рабочего потока типа "красный-зеленый-рефакторинг" (red-green-refactor)

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

  1. Определите, что приложение нуждается в добавлении новой функциональной возможности или метода.

  2. Создайте тест, который проверит поведение новой функциональной возможности после ее реализации.

  3. Выполните тест и получите "красный свет".

  4. Напишите код, который реализует новую функциональную возможность.

  5. Снова запустите тест и корректируйте код до тех пор, пока не получите "зеленый свет".

  6. При необходимости проведите рефакторинг кода - например, реорганизуйте операторы, переименуйте переменные и т.д.

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

Этот рабочий поток повторяется для каждой добавляемой функциональной возможности. В TDD традиционный процесс разработки инвертируется: мы начинаем с написания тестов для корректной реализации того или иного функционального средства, зная, что тесты не пройдут. Затем мы реализуем средство, создавая все аспекты его поведения по очереди для прохождения все большего числа тестов.

Такой цикл является сущностью разработки через тестирование. Есть масса веских причин, чтобы рекомендовать ее в качестве применяемого стиля разработки, и не в последнюю очередь потому, что она вынуждает программиста подумать о том, как должно себя вести изменение или усовершенствование, еще до начала написания кода. У вас всегда будет четкая конечная цель и способ проверки того, что эта цель достигнута. А при наличии модульных тестов, которые покрывают остальную часть приложения, можно иметь уверенность в том, что вносимые изменения не модифицируют какие-то другие аспекты поведения.

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

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

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

Интеграционное тестирование

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

Ниже перечислены два наиболее известных средства автоматизации браузеров с открытым кодом, доступные разработчикам .NET:

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

В результате оно может вскрыть проблемы, возникающие вследствие взаимодействия между компонентами - отсюда и термин интеграционное тестирование. А поскольку интеграционное тестирование веб-приложения осуществляется посредством браузера, можно проверить, что аспекты поведения, реализованные на JavaScript, работают ожидаемым образом, что очень трудно выяснить в ходе модульного тестирования.

Этому виду тестирования присущи и некоторые недостатки. Интеграционное тестирование занимает больше времени. А создание тестов и их выполнение требует больших временных затрат. Кроме того, интеграционные тесты могут оказаться хрупкими. Изменение всего одного атрибута id элемента, проверяемого в ходе теста, может привести к тому, что тест не пройдет (как обычно и происходит).

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

Пройди тесты
Лучший чат для C# программистов