Книга: ASP.NET MVC Framework

Тесты и MVC Framework

Тесты и 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, заключается в использовании "подставных" реализаций везде, где применяются реализации, не зависящие от кода, описанного в самом контроллере, а также в создании отдельных тестовых методов для разных сценариев выполнения одного и того же действия контроллера.

Оглавление книги


Генерация: 1.952. Запросов К БД/Cache: 3 / 1
поделиться
Вверх Вниз