ASP.NET MVC 4 в действии

ASP.NET MVC 4 в действии

Джеффри Палермо

Утверждение результатов

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

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

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

Листинг 20-14: Обеспечение индикатора страницы в нашей разметке
<input type="hidden" name="pageId" value="@LocalSiteMap.Screen.Product.Index" />
<h2>Products</h2>

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

Абсолютное значение – это простая строка:

Листинг 20-15: Структура сайта в правильно построенной модели объекта
public static class LocalSiteMap
{
	...
	public static class Screen
	{
		public static class Product
		{
			public static readonly string Index = "productIndex";
		}
	}
}

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

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

Листинг 20-16: Утверждение конкретной страницы
[Test]
public void Should_update_product_price_successfully()
{
	NavigateLink(LocalSiteMap.Nav.Products);
	Browser.Link(Find.ByText("Edit")).Click();
	ForForm<ProductForm>()
			.WithTextBox(form => form.Price, 389.99m)
			.Save();
	CurrentPageShouldBe(LocalSiteMap.Screen.Product.Index);
	...
}

Строка 9: Утверждает положение текущей страницы

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

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

Листинг 20-17: Метод CurrentPageShouldBe
protected void CurrentPageShouldBe(string pageId)
{
	Browser.TextField(Find.ByName("pageId")).Value.ShouldEqual(pageId);
}

Наконец, нам нужно утвердить тот факт, что наше приложение корректно меняет значение цены. Для этого потребуется некоторая дополнительная доработка нашего представления, потому что на данный момент довольно сложно определить местоположение конкретного, привязанного к данным HTML-элемента. Первоначальный тест всего лишь выполнял поиск текста "Price" на всей странице. Но это подразумевает, что наш тест мог бы передаваться, даже если цена не обновлялась, потому что текст, используемый для цены, мог раскрываться для чего-то несвязанного с ней, например, для другого товара, текста версии в верхней части экрана, итоговой корзины товаров и т.д.

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

Листинг 20-18: Использование шаблонов отображения, базирующихся на выражениях
<table>
	<thead>
		<tr>
			<td>Details</td>
			<td>Name</td>
			<td>Manufacturer</td>
			<td>Price</td>
		</tr>
	</thead>
	<tbody>
		@{
			var i = 0;
		}
		@foreach (var product in products)
		{
			<tr>
				<td>@Html.ActionLink("Edit", "Edit",
								new { id = product.Id })</td>
				<td>@Html.DisplayFor(m => m[i].Name)</td>
				<td>@Html.DisplayFor(m => m[i].ManufacturerName)</td>
				<td>@Html.DisplayFor(m => m[i].Price)</td>
			</tr>
			i++;
		}
	</tbody>
</table>

Строка 17: Использует шаблоны, базирующиеся на выражениях

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

Сначала нам необходимо добавить в нашу папку Shared Display Templates новый файл шаблона строк, как это показано на рисунке 20-5.

Рисунок 20-5: Добавление нового шаблона строк

Шаблон String.cshtml модифицируется в следующем листинге с целью включения тега span с унаследованным ID, в котором используется регулярное выражение для перевода первоначального префикса поля в подходящее значение ID HTML.

Листинг 20-19: Обновленный шаблон отображения строк
@using System.Text.RegularExpressions;
@{
	var originalId = ViewData.TemplateInfo.HtmlFieldPrefix;
	var id = Regex.Replace(originalId, @"[^-_:A-Za-z0-9]", "_");
}
<span id="@id">@ViewData.TemplateInfo.FormattedModelValue</span>

В тег span помещается целостное значение, отображаемое с правильно построенным ID, унаследованным от выражения, которое первоначально использовалось для отображения этого шаблона. В предыдущем листинге первоначальное выражение m => m[ i].Name приводило бы к ID "[0]_Name" интервала времени выполнения. Поскольку индекс массива включен в ID интервала, мы можем отделить это конкретное значение модели от любого другого товара, продемонстрированного на экране. Нам не нужно выполнять поиск элементов, соответствующих родовым значениям; мы можем напрямую переходить к корректному отображаемому значению модели.

В нашем тесте мы создаем объект FluentPage. Это абстракция, похожая на FluentForm, которую мы наблюдали ранее, но FluentPage предоставляет способ утверждения информации, корректно отображаемой на нашем экране. В следующем листинге наш тест использует методы ForPage и FindText для утверждения значения цены конкретного товара.

Листинг 20-20: Окончательный код теста, использующий утверждения отображаемого значения, базирующиеся на выражениях
[Test]
public void Should_update_product_price_successfully()
{
	NavigateLink(LocalSiteMap.Nav.Products);
	Browser.Link(Find.ByText("Edit")).Click();
	ForForm<ProductForm>()
			.WithTextBox(form => form.Price, 389.99m)
			.Save();
	CurrentPageShouldBe(LocalSiteMap.Screen.Product.Index);
	ForPage<ProductListModel[]>()
			.FindText(products => products[0].Price, "389.99");
}

Строка 10: Указывает тип модели представления

Строка 11: Находит текстовое значение

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

Метод ForPage создает объект FluentPage, как это показано ниже.

Листинг 20-21: Класс FluentPage
public class FluentPage<TModel>
{
	private readonly IE _browser;
	public FluentPage(IE browser)
	{
		_browser = browser;
	}
	public FluentPage<TModel> FindText<TField>(
			Expression<Func<TModel, TField>> field,
			TField value)
	{
		var name = UINameHelper.BuildIdFrom(field);
		var span = _browser.Span(Find.ById(name));
		span.Text.ShouldEqual(value.ToString());
		return this;
	}
}

Строка 4: Принимает экземпляр IE в конструктор

Строка 8-10: Определяет метод FindText

Строка 12: Создает название из выражения

Строка 13: Находит элемент по имени

Класс FluentPage обладает единственным родовым параметром, TModel, для типа модели представления страницы. Конструктор FluentPage принимает объект IE и сохраняет его в закрытом поле.

Затем мы определяем метод FindText так же, как делали это ранее для метода WithTextBox. FindText содержит родовой параметр относительно типа поля и принимает в качестве параметра единственное выражение для олицетворения приема объекта формы и возврата элемента формы. FindText также принимает ожидаемое значение.

В теле метода нам сначала нужно создать ID из данного выражения. Затем мы находим элемент span, использующий ID, созданный из выражения. Объект span содержит свойство Text, представляющее собой содержимое тега span, и мы утверждаем, что содержимое span совпадает со значением, предоставляемым в методе FluentPage.

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

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

или RSS канал: Что новенького на smarly.net