Простое приложение для модульного тестирования

80

Для начала мы создаем в Visual Studio новый проект по имени TestAspNet45, используя шаблон ASP.NET Empty Web Application (Пустое веб-приложение ASP.NET), как это делалось в статье «Простое приложение ASP.NET Web Forms 4.5». В рассмотренном ранее примере все файлы помещались в корневую папку проекта, но в измененному проекту мы хотим придать определенную структуру. Для этого мы создаем несколько дополнительных папок, которые можно видеть в окне Solution Explorer (Проводник решения) на рисунке ниже:

Добавление структуры папок в проект

Добавленными папками являются Content, Models, Models\Repository, Pages, Presenters и Presenters\Results. Мы будем применять их для разбиения проекта на отдельные части, как будет показано далее.

Установка статического контента

Статический контент HTML и CSS размещается в папке Content. Мы планируем воссоздать функциональность и контент приложения, созданного в статье «Простое приложение ASP.NET Web Forms 4.5». Это означает, что мы должны начать с добавления ряда файлов в папку Content. В примере ниже показаны стили CSS, определенные в файле \Content\Styles.css:

#rsvpform label {
    width: 120px;
    display: inline-block;
}

#rsvpform input {
    margin: 2px;
    margin-left: 4px;
    width: 150px;
}

#rsvpform select {
    margin: 2px 0;
    width: 154px;
}

button[type=submit] {
    margin-top: 15px;
    padding: 5px;
}

table, td, th {
    border: thin solid black; 
    border-collapse: collapse; 
    padding: 5px;
    background-color: lemonchiffon; 
    text-align: left; 
    margin: 10px 0;
}

#validationSummary {
    color: red;
}

В примере ниже представлена разметка из файла \Content\seeyouthere.html:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>Увидимся на вечеринке!</title>
</head>
<body>
    <h1>Увидимся на вечеринке! ;)</h1>
    <p>Приходите в 9 вечера. Маски являются обязательными!</p>
</body>
</html>

В примере ниже приведено содержимое файла \Content\sorryyoucantcome.html:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>Жаль что вы не сможете прийти!</title>
</head>
<body>
    <h1>Жаль что вы не сможете прийти :(</h1>
    <p>Увидимся в следующем году!</p>
</body>
</html>

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

Установка модели данных

Классы модели данных размещаются в папке Models. В приложении TestAspNet45 имеется только один класс модели данных, и никаких изменений в его определение вносить не понадобится. Создайте новый файл класса \Models\GuestResponse.cs и приведите его содержимое в соответствие с примером ниже:

using System.ComponentModel.DataAnnotations;

namespace TestAspNet45.Models
{
    public class GuestResponse
    {
        [Required]
        public string Name { get; set; }

        [Required]
        public string Email { get; set; }

        [Required]
        public string Phone { get; set; }

        [Required(ErrorMessage="Пожалуйста укажите, придете ли вы")]
        public bool? WillAttend { get; set; }
    }
}

Реализация хранилища

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

using System;
using System.Collections.Generic;

namespace TestAspNet45.Models.Repository
{
    public interface IRepository
    {
        IEnumerable<GuestResponse> GetAllResponses();
        void AddResponse(GuestResponse response);
    }
}

Чтобы быстро создать интерфейс, щелкните правой кнопкой мыши на папке Models, выберите в контекстном меню пункт Add --> New Item (Добавить --> Новый элемент) и укажите шаблон элемента Interface (Интерфейс).

Хранилище для этого приложения выглядит очень просто, так что в интерфейсе IRepository определены только методы для извлечения всех объектов данных GuestResponse и для добавления нового объекта данных GuestResponse.

Чтобы создать реализацию интерфейса IRepository, мы добавили в папку Models\Repository новый файл класса под названием MemoryRepository.cs, содержимое которого показано в примере ниже. (Мы назначаем классам хранилища имена, отражающие механизм, с помощью которого хранятся объекты модели данных. В этом случае применяется память, т.е. сохраненные данные будут утеряны в результате останова или перезапуска приложения.)

using System.Collections.Generic;

namespace TestAspNet45.Models.Repository
{
    public class MemoryRepository : IRepository
    {
        private List<GuestResponse> responses = new List<GuestResponse>();

        public IEnumerable<GuestResponse> GetAllResponses()
        {
            return responses;
        }

        public void AddResponse(GuestResponse response)
        {
            responses.Add(response);
        }
    }
}

В разделе, посвященном созданию интернет-магазина на ASP.NET Web Forms, мы добавляли к классу хранилища статический метод GetRepository() чтобы можно было использовать единственный экземпляр хранилища по всему приложению. В этой статье мы собираемся применить другой подход, который опирается на прием, называемый внедрением зависимостей. Мы продемонстрируем этот прием после построения большей части инфраструктуры приложения.

Добавление инфраструктуры

Чтобы можно было реализовать наш шаблон, мы должны добавить в приложение некоторую инфраструктуру. Нам нужна возможность ассоциирования веб-страниц, исполняющих роль представлений в шаблоне, с классами презентаторов, не создавая между ними жестких зависимостей. Это означает, что необходимо определить интерфейс, задающий базовую функциональность презентатора, для чего создается файл Presenters\Presenter.cs с интерфейсом IPresenter<T>, который показан в примере ниже:

using TestAspNet45.Presenters.Results;

namespace TestAspNet45.Presenters
{
    public interface IPresenter<T>
    {
        IResult GetResult();
        IResult GetResult(T requestData);
    }
}

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

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

Оба метода предоставляют руководство относительно действия, которое представление должно выполнить для генерации ответа, возвращая реализацию интерфейса IResult. Этот интерфейс определен в файле Presenters\Results\IResnlts.cs, как показано ниже:

namespace TestAspNet45.Presenters.Results
{
    public interface IResult
    {
    }
}

Это всего лишь пустой интерфейс, который можно применять для реализации различных видов действий, предназначенных для выполнения представлением. В примере проекта определены две реализации IResult. Первая их них - класс RedirectResult - приведена в примере ниже; определение находится в файле Presenters\Results\RedirectResult.cs:

namespace TestAspNet45.Presenters.Results
{
    public class RedirectResult : IResult
    {
        private string url;
        public RedirectResult(string urlValue)
        {
            url = urlValue;
        }

        public string Url
        {
            get { return url; }
        }
    }
}

Этот класс будет использоваться для указания на необходимость перенаправления браузера пользователя куда-то в другое место. Целевой URL передается конструктору класса RedirectResuIt и доступен через свойство, предназначенное только для чтения.

Кроме того, в файле \Presenters\Results\DataResult.cs определен класс DataResult, который показан ниже:

namespace TestAspNet45.Presenters.Results
{
    public class DataResult<T> : IResult
    {
        private T dataItem;

        public DataResult(T data)
        {
            dataItem = data;
        }

        public T DataItem
        {
            get { return dataItem; }
        }
    }
}

Параметр обобщенного типа в классе DataResult применяется для указания объекта данных, который представление должно отобразить. Использование параметра типа в противоположность object поможет проверить получение результата ожидаемого типа от бизнес-логики.

Реализация страницы RSVP

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

Создание презентатора

Первым делом мы создадим презентатор, который будет содержать бизнес-логику для поддержки ответов RSVP, отправляемых пользователями. Мы создали новый файл класса по имени \Presenters\RSVPPresenter.cs с содержимым, которое приведено ниже:

using TestAspNet45.Models;
using TestAspNet45.Models.Repository;
using TestAspNet45.Presenters.Results;

namespace TestAspNet45.Presenters
{
    public class RSVPPresenter
    {
    }
}

Мы собираемся построить этот класс поэтапно, чтобы сделать процесс предельно ясным. Выше показано начальное определение класса. Следующий шаг заключается в объявлении того, что класс PSVPPresenter реализует интерфейс IPresenter<T>:

using System;
using TestAspNet45.Models;
using TestAspNet45.Models.Repository;
using TestAspNet45.Presenters.Results;

namespace TestAspNet45.Presenters
{
    public class RSVPPresenter : IPresenter<GuestResponse>
    {

    }
}

Для указания того, что этот класс будет оперировать с объектами GuestResponse, мы применили параметр обобщенного типа. На следующем шаге мы явно реализуем интерфейс. Проще всего это сделать, щелкнув правой кнопкой мыши на имени IPresenter в коде и выбрав в контекстном меню пункт Implement Interface --- Implement Interface Explicitly (Интерфейс --- Реализовать интерфейс явно). В примере ниже можно увидеть код, который среда Visual Studio добавила в класс RSVPPresenter для поддержки этого интерфейса:

using System;
using TestAspNet45.Models;
using TestAspNet45.Models.Repository;
using TestAspNet45.Presenters.Results;

namespace TestAspNet45.Presenters
{
    public class RSVPPresenter : IPresenter<GuestResponse>
    {
        IResult IPresenter<GuestResponse>.GetResult()
        {
            throw new NotImplementedException();
        }

        IResult IPresenter<GuestResponse>.GetResult(GuestResponse requestData)
        {
            throw new NotImplementedException();
        }
    }
}

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

Осталось лишь реализовать в методах интерфейса необходимую бизнес-логику, как показано в примере ниже:

using System;
using TestAspNet45.Models;
using TestAspNet45.Models.Repository;
using TestAspNet45.Presenters.Results;

namespace TestAspNet45.Presenters
{
    public class RSVPPresenter : IPresenter<GuestResponse>
    {
        public IRepository repository { get; set; }

        IResult IPresenter<GuestResponse>.GetResult()
        {
            return new DataResult<GuestResponse>(new GuestResponse());
        }

        IResult IPresenter<GuestResponse>.GetResult(GuestResponse requestData)
        {
            repository.AddResponse(requestData);
            if (requestData.WillAttend.Value)
                return new RedirectResult(@"/Content/seeyouthere.html");
            else
                return new RedirectResult(@"/Content/sorryyoucantcome.html");
        }
    }
}

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

При вызове другой перегруженной версии метода GetResult() параметр GuestResponse помещается в хранилище и возвращается объект RedirectResult, который указывает, что браузер должен быть перенаправлен. Для доступа в хранилище определено свойство repository, которое должно быть установлено перед применением класса.

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

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

Мы создали в папке Pages новую веб-форму по имени Default.aspx и добавили отсканированный с помощью копицентра http://copy.spb.ru/nahi_uslugi/skanirovanie/ текст для вечеринки, который добавили в HTML-разметку:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs"
    Inherits="TestAspNet45.Pages.Default" %>

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
    <link rel="stylesheet" href="/Content/Styles.css" />
</head>
<body>
    <form id="rsvpform" runat="server">
        <div>
            <h1>Новый год у Татьяны!</h1>
            <p>Мы устроим классную вечеринку и вы приглашены!</p>
        </div>
        <asp:ValidationSummary ID="validationSummary" runat="server" ShowModelStateErrors="true" />
        <div>
            <label>Ваше имя:</label><input type="text" id="name" runat="server" /></div>
        <div>
            <label>Ваш email:</label><input type="text" id="email" runat="server" /></div>
        <div>
            <label>Ваш телефон:</label><input type="text" id="phone" runat="server" /></div>
        <div>
            <label>Вы придете?</label>
            <select id="willattend" runat="server">
                <option value="">Выберите один из вариантов</option>
                <option value="true">Да</option>
                <option value="false">Нет</option>
            </select>
        </div>
        <div>
            <button type="submit">Отправить приглашение RSVP</button>
        </div>
    </form>
</body>
</html>

Здесь есть пара отличий по сравнению с соответствующим файлом из рассмотренного ранее примера. Во-первых, изменено значение атрибута Inherits внутри директивы Page, находящейся в начале файла, чтобы отразить новое пространство имен. (Среда Visual Studio будет устанавливать это автоматически, но если вы копируете код из файлов примера, то должны обращать особое внимание на указанное пространство имен.) Во-вторых, элемент link ссылается на файл Styles.css, расположенный в папке Content. Во всем остальном разметка совпадает.

В примере ниже показано содержимое файла отделенного кода \Pages\Default.aspx.cs. Именно в нем применяется презентатор для поддержки простой бизнес-логики, так что веб-форма может исполнять роль представления и оставаться ориентированной на браузерный запрос и ответ.

using System;
using System.Web.ModelBinding;
using TestAspNet45.Models;
using TestAspNet45.Models.Repository;
using TestAspNet45.Presenters;
using TestAspNet45.Presenters.Results;

namespace TestAspNet45.Pages
{
    public partial class Default : System.Web.UI.Page
    {
        public IPresenter<GuestResponse> Presenter { get; set; }

        protected void Page_Load(object sender, EventArgs e)
        {
            Presenter = new RSVPPresenter { repository = new MemoryRepository() };

            if (IsPostBack)
            {
                GuestResponse rsvp = 
                    ((DataResult<GuestResponse>)Presenter.GetResult()).DataItem;

                if (TryUpdateModel(rsvp, new FormValueProvider(ModelBindingExecutionContext)))
                {
                    Response.Redirect(((RedirectResult)Presenter.GetResult(rsvp)).Url);
                }
            }
        }
    }
}

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

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

Тестирование страницы RSVP

Мы добрались до точки, когда появилась возможность протестировать созданную страницу. Щелкните правой кнопкой мыши на файле \Pages\Default.aspx в окне Solution Explorer и выберите в контекстном меню пункт Set as Start Page (Установить в качестве стартовой страницы), чтобы браузер загружал эту веб-форму автоматически. Запустите приложение либо через панель инструментов Visual Studio, либо выбрав пункт Start Debugging (Начать отладку) в меню Debug (Отладка), и вы увидите знакомый контент:

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