ASP.NET MVC Framework

Магдануров Гайдар

Юнев Владимир

ГЛАВА 8

Тестирование веб-приложений

 

 

 В главах 1 и 2 книги было особо отмечено, что простота автоматизированного тестирования является одним из значительных преимуществ MVC Framework над WebForms. Пришло время рассмотреть более детально процесс автоматического тестирования веб-приложений, созданных на базе MVC Framwork.

В этой главе мы воспользуемся популярным решением с открытым кодом для создания автоматических тестов — NUnit, которое стало стандартом "де-факто" для многих команд веб-разработчиков на платформе Microsoft .NET.

 

Установка и настройка NUnit

Для начала необходимо загрузить и установить оснастку тестирования NUnit (). Затем установить шаблоны проектов тестирования (), для этого нужно загрузить архив NUnit Test Templates, скопировать содержимое директории CSharp в директорию \Documents\Visual Studio 2008\Templates\ProjectTemplates\Visual C# (аналогично для директории Visual Basic, если вы используете язык Visual Basic), после чего загрузить данные из REG-файла в реестр для регистрации шаблона NUnit в мастере создания проектов ASP.NET MVC Framework.

Когда вы завершите процедуры установки NUnit и шаблонов проектов, при создании нового проекта ASP.NET MVC Framework появится возможность создать проект для тестирования с использованием NUnit (рис. 8.1).

После создания проекта вместе с проектом веб-приложения будет создан проект библиотеки классов, являющийся основной сборкой для тестирования веб-приложения, представленный на рис. 8.2.

Рис. 8.2. Проект тестирования веб-приложения

В случае если необходимо создать проект тестирования для уже существующего проекта, достаточно добавить в решение проект библиотеки классов, добавить в него ссылку на тестируемый проект и ссылку на сборку nunit.framework.dll, находящуюся в директории bin в директории установки NUnit, и создать тесты так, как будет показано далее в этой главе.

 

Создание и выполнение тестов

Набор простейших тестов, проверяющий корректность работы контроллера Home из шаблона веб-приложения, приведен в листинге 8.1.

Листинг 8.1. Тесты контроллера Ноше

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Web.Mvc;

using NUnit.Framework;

using MvcTestApp;

using MvcTestApp.Controllers;

namespace MvcTestApp.Tests.Tests.Controllers

  {

  [TestFixture]

  public class HomeControllerTest

  {

    [Test]

    public void Index()

    {

      HomeController controller = new HomeController();

      ViewResult result = controller.Index() as ViewResult;

      ViewDataDictionary viewData = result.ViewData;

      Assert.AreEqual("Welcome to ASP.NET MVC!",

      viewData["Message"]);

    }

    [Test]

    public void About()

    {

      HomeController controller = new HomeController();

      ViewResult result = controller.About() as ViewResult; Assert.IsNotNull(result);

    }

  }

}

Тесты представляют собой методы, помеченные атрибутом Test, помещенные в классы, помеченные атрибутом TestFixture. Среда выполнения тестов NUnit находит в сборке с тестами тестирующие методы именно по этим атрибутам.

Структура каждого теста может быть разбита на три основных элемента:

□ подготовка данных и объектов для проведения теста;

□ выполнение действия, результат которого нужно протестировать;

□ проверка результатов выполнения действия.

Рассмотрим тестовый метод Index из листинга 8.1, проверяющий одноименное действие контроллера Home.

Подготовка данных и объектов в этом случае представляет собой только лишь создание экземпляра класса HomeController.

HomeController controller = new HomeController();

Выполнение действия — вызов метода Index у созданного ранее объекта controller, возвращающего результат типа viewResult.

ViewResult result = controller.Index() as ViewResult;

Проверка результата в данном случае заключается в проверке значения, помещенного в коллекцию ViewData с ключом Message с помощью метода класса Assert, используемого для целей отладки и тестирования.

Assert.AreEqual("Welcome to ASP.NET MVC!", viewData["Message"]);

После того как тесты созданы, сборку, содержащую тесты, нужно скомпилировать и загрузить в графическую оболочку NUnit. Оболочка распознает методы, помеченные атрибутами Test, и выводит их в виде дерева тестов, как это показано на рис. 8.3.

Тесты можно выполнять по одному, либо блоками, относящимися к одному классу, содержащему тестирующие методы, либо все тесты сразу (рис. 8.4).

Теперь представим себе, что вследствие развития проекта результат выполнения метода Index изменился, и в коллекцию ViewData с ключом Message помещается другая строка.

public ActionResult Index()

{

  ViewData["Message"] = "Всем привет!";

  return View();

}

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

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

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

 

Несколько слов о важности тестирования

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

Примечание

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

*****************************

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

 

Тесты и MVC Framework

После прочтения предыдущей части главы можно было убедиться, что создание теста логики веб-приложения, построенного на базе MVC Framework, — задача простая. Этот эффект достигается за счет того, что ядро веб-приложения, состоящее из контроллеров и слоя работы с данными (моделей), может быть выделено в независимую от среды сборку и тестироваться вне контекста веб-сервера и без генерации разметки представления. Даже в тех случаях, когда среде выполнения необходимо взаимодействие с объектами, отвечающими за ввод/вывод разметки и запросов, передаваемых между пользователем и сервером, используются абстрактные классы (например, HttpRequestBase), реализация которых может быть подменена в тестовой среде.

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

Для поддержки возможности замены конкретной реализации необходимо использовать интерфейсы для основных зависимостей действий контроллеров. Например, если в приложении есть выделенный объект, отвечающий за восстановление и сохранение объектов данных DataManager, то в контроллерах следует использовать интерфейс iDataManager, который реализует конкретный класс DataManager. Таким образом, во время проведения тестов объект DataManager может быть подменен объектом DataManagerMock, не содержащим полного функционала работы с данными и, возможно, даже не работающим с базой данных. Этот распространенный подход к тестированию называется имитацией (англ. moking).

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

Рассмотрим создание более сложного теста на примере тестового сценария для контроллера Accountcontroller, входящего в шаблон MVC-приложения. В листинге 8.2 приведен фрагмент кода Accountcontroller, демонстрирующий использование интерфейсов в конструкторах класса.

Листинг 8.2. Фрагмент кода класса Accountcontroller

public class Accountcontroller : Controller

{

  public Accountcontroller() : this(null, null)

  {

  }

  public AccountController(IFormsAuthentication formsAuth,

           IMembershipService service)

  {

    FormsAuth = formsAuth ?? new FormsAuthenticationService();

    MembershipService = service ?? new AccountMembershipService();

  }

  public IFormsAuthentication FormsAuth

  {

    get;

    private set;

  }

  public IMembershipService MembershipService

  {

    get;

    private set;

  }

}

Поскольку в контроллере Accountcontroller используются компоненты инфраструктуры ASP.NET, отвечающие за аутентификацию пользователей и управление учетными записями, для них созданы специальные обертки, приведенные в листинге 8.3, реализующие интерфейсы IFormsAuthentication и IMembershipService.

Листинг 8.3. Интерфейсы и реализация поддержки базовых служб

public interface IFormsAuthentication

{

  void SignIn(string userName, bool createPersistentcookie);

  void SignOut();

}

public class FormsAuthenticationService : IFormsAuthentication

{

  public void SignIn(string userName, bool createPersistentCookie)

  {

    FormsAuthentication.SetAuthCookie(userName,

                  createPersistentCookie);

  }

  public void SignOut()

  {

    FormsAuthentication.SignOut();

  }

}

public interface IMembershipService

{

  int MinPasswordLength { get; }

  bool ValidateUser(string userName, string password);

  MembershipCreateStatus CreateUser(string userName,

        string password, string email);

  bool ChangePassword(string userName,

        string oldPassword, string newPassword);

}

public class AccountMembershipService : IMembershipService

{

  private MembershipProvider _provider;

  public AccountMembershipService()

              : this(null)

  {

  }

  public AccountMembershipService(MembershipProvider provider)

  {

    _provider = provider ?? Membership.Provider;

  }

  public int MinPasswordLength

  {

    get

    {

      return _provider.MinRequiredPasswordLength;

    }

  }

  public bool ValidateUser(string userName, string password)

  {

    return _provider.ValidateUser(userName, password);

  }

  public MembershipCreateStatus CreateUser(string userName,

                 string password, string email)

  {

    MembershipCreateStatus status;

    _provider.CreateUser(userName, password,

          email, null, null, true, null, out status);

    return status;

  }

  public bool ChangePassword(string userName,

            string oldPassword, string newPassword)

  {

    MembershipUser currentUser = _provider.GetUser(

            userName, true);

    return currentUser.ChangePassword(oldPassword, newPassword);

  }

}

Выделение обертки над службами ASP.NET позволяет не только получить возможность простого тестирования, но и заменять реализацию в процессе развития приложения. Например, при замене стандартного провайдера Membership на собственную реализацию или изменение провайдера авторизации для поддержки сертификатов, или Windows-аутентификация. Однако в целях тестирования этот подход просто незаменим.

Во время работы веб-приложения, созданного из шаблона, используется стандартная фабрика контроллеров, не передающая дополнительных параметров конструктору класса Accountcontroller, в тестовом же окружении используется имитация — код классов, имитирующих бурную деятельность. Пример классов, реализующих подход имитации, приведен в листинге 8.4.

Листинг 8.4. Классы для имитации в условиях тестирования

public class MockFormsAuthenticationService :

          IFormsAuthentication

{

  public void SignIn(string userName,

      bool createPersistentcookie) { }

  public void SignOut() { }

}

public class MockHttpContext : HttpContextBase

{

  private IPrincipal _user;

  public override IPrincipal User

  {

    get

    {

      if (_user == null)

      {

        _user = new MockPrincipal();

      }

      return _user;

    }

    set

    {

      _user = value;

    }

  }

}

Аналогично коду, приведенному в листинге 8.4, строятся остальные классы, используемые для тестирования. Основной принцип их создания состоит в том, чтобы развязать процесс тестирования с реальным функционалом — вместо работы с базами данных не делать ничего, либо возвращать стандартные значения, используемые для тестирования, и т. п.

Пример использования имитирующих классов в целях тестирования приведен в листинге 8.5. Процесс подготовки объектов выделен в отдельный метод GetAccountController() .

Листинг 8.5. Несколько методов для тестирования AccountController

private static AccountController GetAccountController()

{

  IFormsAuthentication formsAuth = new

                 MockFormsAuthenticationService();

  MembershipProvider membershipProvider = new MockMembershipProvider();

  AccountMembershipService membershipService = new

  AccountMembershipService(membershipProvider);

  AccountController controller = new

  AccountController(formsAuth, membershipService);

  ControllerContext controllerContext = new

      ControllerContext(new MockHttpContext(),

                  new RouteData(),

                  controller);

  controller.ControllerContext = controllerContext;

  return controller;

}

[Test]

public void RegisterPostReturnsViewIfPasswordIsNull()

{

  AccountController controller = GetAccountController();

  ViewResult result = (ViewResult)controller.Register("username",

             "email", null, null);

  Assert.AreEqual(6, result.ViewData["PasswordLength"]);

  Assert.AreEqual(

    "You must specify a password of 6 or more characters.",

    result.ViewData.ModelState["password"].Errors[0].ErrorMessage);

}

[Test]

public void

    RegisterPostReturnsViewIfNewPasswordDoesNotMatchConfirmPassword()

{

  AccountController controller = GetAccountController();

  ViewResult result = (ViewResult)controller.Register("username",

                            "email", "password", "password2");

  Assert.AreEqual (6, result.ViewData["PassworcLLength"] );

  Assert.AreEqual(

      "The new password and confirmation password do not match.",

      result.ViewData.ModelState["_FORM"].Errors[0].ErrorMessage);

}

[Test]

public void RegisterPostReturnsViewIfPasswordIsTooShort()

{

  AccountController controller = GetAccountController();

  ViewResult result = (ViewResult)controller.Register("username",

                "email", "12345", "12345");

  Assert.AreEqual(6, result.ViewData["PasswordLength"]);

  Assert.AreEqual(

      "You must specify a password of 6 or more characters.",

      result.ViewData.ModelState["password"].Errors[0].ErrorMessage);

}

[Test]

public void RegisterPostReturnsViewIfRegistrationFails()

{

  AccountController controller = GetAccountController();

  ViewResult result = (ViewResult)controller.Register("someUser",

    "DuplicateUserName" /* error */, "badPass", "badPass");

  Assert.AreEqual(6, result.ViewData["PasswordLength"]);

  Assert.AreEqual(

      "Username already exists. Please enter a different user name.",

      result.ViewData.ModelState["_FORM"].Errors[0].ErrorMessage);

}

[Test]

public void RegisterPostReturnsViewIfUsernameNotSpecified()

{

  AccountController controller = GetAccountController();

  ViewResult result = (ViewResult)controller.Register("", "email",

                "password", "password");

  Assert.AreEqual(6, result.ViewData["PasswordLength"]);

  Assert.AreEqual("You must specify a username.",

      result.ViewData.ModelState["username"].Errors[0].ErrorMessage);

}

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

 

Заключение

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