Главная страница   /   3.5. Приступим к работе с автоматизированным тестированием (ASP.NET MVC 4

ASP.NET MVC 4

ASP.NET MVC 4

Адам Фриман

3.5. Приступим к работе с автоматизированным тестированием

ASP.NET MVC Framework разработан так, чтобы как можно сильнее облегчить создание автоматизированных тестов и использование таких методологий разработки, как TDD, о чем мы поговорим далее в этой главе. ASP.NET MVC обеспечивает идеальную платформу для автоматизированного тестирования, а в Visual Studio есть несколько отличных функций тестирования, которые упрощают создание и выполнение тестов.

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

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

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

Юнит тестирование

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

Примечание

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

В листинге 3-7 содержится пример тестовой фикстуры, которая проверяет поведение метода AdminContoller.ChangeLoginName, что мы определили в листинге 3-6.

Листинг 3-7: Пример тестовой фикстуры
[TestClass]
public class AdminControllerTest
{
	[TestMethod]
	public void CanChangeLoginName()
	{
		// Arrange (устанавливается сценарий)
		Member bob = new Member() { LoginName = "Bob" };
		FakeMembersRepository repositoryParam = new FakeMembersRepository();
		repositoryParam.Members.Add(bob);
		AdminController target = new AdminController(repositoryParam);
		string oldLoginParam = bob.LoginName;
		string newLoginParam = "Anastasia";
		// Act (проводится операция)
		target.ChangeLoginName(oldLoginParam, newLoginParam);
		// Assert (проверяется результат)
		Assert.AreEqual(newLoginParam, bob.LoginName);
		Assert.IsTrue(repositoryParam.DidSubmitChanges);
	}
	private class FakeMembersRepository : IMembersRepository
	{
		public List<Member> Members = new List<Member>();
		public bool DidSubmitChanges = false;
		public void AddMember(Member member)
		{
			throw new NotImplementedException();
		}
		public Member FetchByLoginName(string loginName)
		{
			return Members.First(m => m.LoginName == loginName);
		}
		public void SubmitChanges()
		{
			DidSubmitChanges = true;
		}
	}
}

Тестовая фикстура – это метод CanChangeLoginName. Обратите внимание, что в методе присутствует атрибут TestMethod, и что в классе, к которому он принадлежит, AdminControllerTest, есть атрибут TestClass: это способ, как Visual Studio находит тестовую фикстуру.

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

Тестовая фикстура в листинге 3-7 использует специальную тестовую симулирующую реализацию интерфейса IMembersRepository для имитации определенных условий, в данном случае, когда в репозитории есть один член, Bob. Создание имитационного репозитория и Member сделано в разделе arrange теста.

Далее, вызывается тестируемый метод AdminController.ChangeLoginName. Это часть act теста. И наконец, мы проверяем результаты с помощью пары вызовов Assert (это assert часть теста). Мы запускаем тест с помощью Visual Studio меню Test и получаем визуальное представление о том, как выполняются тесты, что показано на рисунке 3-9.

Рисунок 3-9: Визуальное представление о прогрессе юнит теста

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

Примечание

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

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

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

Использование TDD и процесс Red-Green-Refactor

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

  • Определяем, что нам нужно добавить новую функцию или метод в приложение.
  • Пишем тест, который будет проверять поведение новой функции, когда она написана.
  • Запускаем тест и получаем галочку на красном фоне.
  • Пишем код, который реализует новую функцию.
  • Снова запускаем тест и корректируем код, пока не появится зеленый фон.
  • Если требуется, оптимизируем код (проводим рефакторинг), например, реорганизация выражений, переименование переменных и так далее.
  • Запускаем тест, чтобы подтвердить, что изменения не повлияли на поведение дополнений.

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

Листинг 3-8: Добавление метода-заглушки для класса Item
using System;
using System.Collections.Generic;
namespace TheMVCPattern.Models
{
	public class Item
	{
		public int ItemID { get; private set; } // Уникальный ключ
		public string Title { get; set; }
		public string Description { get; set; }
		public DateTime AuctionEndDate { get; set; }
		public IList<Bid> Bids { get; private set; }
		public void AddBid(Member memberParam, decimal amountParam)
		{
			throw new NotImplementedException();
		}
	}
}

Очевидно, что метод AddBid, выделенный жирным шрифтом, не отображает требуемое поведение, но пусть это вас не останавливает – ключом к TDD является проверка на правильное поведения до реализации этой функции. Мы собираемся протестировать три различных аспекта поведения, которое мы хотим реализовать:

  • Когда нет ставок, может быть добавлено любое значение.
  • Когда ставки есть, может быть добавлено более высокое значение.
  • Когда ставки есть, более низкое значение не может быть добавлено.

Что бы сделать это, мы создадим три тесовых метода, как показано в листинге 3-9.

Листинг 3-9: Три тестовые фикстуры
[TestMethod()]
public void CanAddBid() {
	// Arrange - устанавливается сценарий
	Item target = new Item();
	Member memberParam = new Member();
	Decimal amountParam = 150M;
	// Act - выполняется тест
	target.AddBid(memberParam, amountParam);
	// Assert - проверяется поведение
	Assert.AreEqual(1, target.Bids.Count());
	Assert.AreEqual(amountParam, target.Bids[0].BidAmount);
}

[TestMethod()]
[ExpectedException(typeof(InvalidOperationException))]
public void CannotAddLowerBid() {
	// Arrange
	Item target = new Item();
	Member memberParam = new Member();
	Decimal amountParam = 150M;
	// Act
	target.AddBid(memberParam, amountParam);
	target.AddBid(memberParam, amountParam - 10);
}

[TestMethod()]
public void CanAddHigherBid() {
	// Arrange
	Item target = new Item();
	Member firstMember = new Member();
	Member secondMember = new Member();
	Decimal amountParam = 150M;
	// Act
	target.AddBid(firstMember, amountParam);
	target.AddBid(secondMember, amountParam + 10);
	// Assert
	Assert.AreEqual(2, target.Bids.Count());
	Assert.AreEqual(amountParam + 10, target.Bids[1].BidAmount);
}

Вы видите, что мы создали модульный тест для каждого из типов поведения, которые мы хотим видеть. Тестовые методы следуют паттерну arrange/act/assert для создания, тестирования и проверки одного из аспектов общего поведения. Метод CannotAddLowerBid не имеет утвержденной части в теле метода, поскольку успешный тест – это исключение, которые мы утверждаем, применяя атрибут ExpectedException тестового метода.

Примечание

Обратите внимание, как тест, который мы выполняем в методе юнит теста CannotAddLowerBid будут формировать реализацию метода AddBid. Мы валидируем результат теста, гарантируя, что есть исключение и что оно является экземпляром System.InvalidOperationException. Написание юнит тестов перед написанием кода может помочь вам подумать о том, какие результаты должны быть выражены, прежде чем увязнуть в реализации.

Как и следовало ожидать, все эти тесты не сработают, когда мы их запустим, как показано на рисунке 3-10.

Рисунок 3-10: Первый запуск юнит тестов

Теперь может быть реализован наш первый переход к методу AddBid, как показано в листинге 3-10.

Листинг 3-10: Реализация метода AddBid
using System;
using System.Collections.Generic;
namespace TheMVCPattern.Models
{
	public class Item
	{
		public int ItemID { get; private set; } // Уникальный ключ
		public string Title { get; set; }
		public string Description { get; set; }
		public DateTime AuctionEndDate { get; set; }
		public IList<Bid> Bids { get; set; }
		public Item()
		{
			Bids = new List<Bid>();
		}
		public void AddBid(Member memberParam, decimal amountParam)
		{
			Bids.Add(new Bid()
			{
				BidAmount = amountParam,
				DatePlaced = DateTime.Now,
				Member = memberParam
			});
		}
	}
}

Мы добавили первоначальную реализацию метода AddBid классу Item. Мы также добавили простой конструктор, поэтому мы можем создавать экземпляры Item и знать, что коллекция объектов Bid правильно инициализирована. Новый запуск юнит тестов покажет более хорошие результаты, как представлено на рисунке 3-11.

Рисунок 3-11: Новый запуск юнит тестов

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

Листинг 3-11: Улучшение реализации метода AddBid
using System;
using System.Collections.Generic;
using System.Linq;
namespace TheMVCPattern.Models
{
	public class Item
	{
		public int ItemID { get; private set; } // Уникальный ключ
		public string Title { get; set; }
		public string Description { get; set; }
		public DateTime AuctionEndDate { get; set; }
		public IList<Bid> Bids { get; set; }
		public Item()
		{
			Bids = new List<Bid>();
		}
		public void AddBid(Member memberParam, decimal amountParam)
		{
			if (Bids.Count() == 0 || amountParam > Bids.Max(e => e.BidAmount))
			{
				Bids.Add(new Bid()
				{
					BidAmount = amountParam,
					DatePlaced = DateTime.Now,
					Member = memberParam
				});
			}
			else
			{
				throw new InvalidOperationException("Bid amount too low");
			}
		}
	}
}

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

Примечание

Мы использовали Language Integrated Query (LINQ), чтобы убедиться, что ставка действительна. Не волнуйтесь, если вы не знакомы с LINQ или лямбда-выражениями, которые мы использовали (обозначение =>): мы познакомим вас с функциями C#, которые имеют важное значение для разработки с MVC, в главе 6.

Каждый раз, когда мы меняем реализацию метода AddBid, мы заново запускаем наши юнит тесты. Результаты показаны на рисунке 3-12.

Рисунок 3-12: Успешные результаты юнит тестов

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

Этот цикл является сущностью TDD. Существует множество причин, чтобы рекомендовать его как стиль программирования: не в последнюю очередь потому, что он заставляет программиста подумать о том, как изменение или расширение должно вести себя, перед началом кодирования. У вас всегда есть четкое представление о том, что происходит. И если у вас есть юнит тесты, которые покрывают остальную часть вашего приложения, вы будете уверены, что ваши дополнения не изменили поведение других местах.

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

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

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

Мы оба стали приверженцами модульного тестирования и убеждены, что это отличный стиль разработки. ASP.NET MVC является идеальным кандидатом для принятия модульного тестирования, если вы никогда не пробовали его раньше или если вы попробовали и отказались. Команда Microsoft сделала модульное тестирование невероятно легким, разделяя основные классы от базовой технологии. Это означает, мы можем создавать mock-объекты для ключевых функций и тестировать ситуации, которые было бы невероятно трудно воспроизвести в ином случае. Мы покажем вам в этой книге примеры модульного тестирования MVC приложений. Мы рекомендуем вам следовать с нами и попробовать модульное тестирование для своих приложений.

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

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

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

  • Selenium RC (http://seleniumhq.org/), состоящий из "серверного" Java приложения. Он может отправлять команды автоматизации для Internet Explorer, Firefox, Safari или Opera. Также он поддерживает .NET, Python, Ruby и так далее, так что вы можете писать тестовые скрипты на языке по вашему выбору. Selenium является мощным и зрелым инструментом, его единственный недостаток состоит в том, что вы должны запускать его Java сервер.
  • WatiN (http://watin.org), .NET библиотека, которая может посылать команды для автоматизации Internet Explorer или Firefox. Его API не является столь мощным, как Selenium,, но он удобно обрабатывает наиболее распространенные сценарии и прост в настройке: вам нужно ссылаться только на одну DLL.

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

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

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

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