Главная страница   /   4.3. Знакомство с модульным тестированием (ASP.NET MVC 4 в действии

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

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

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

4.3. Знакомство с модульным тестированием

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

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

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

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

Использование встроенного проекта теста

По умолчанию при создании нового ASP.NET MVC проекта Visual Studio предоставляет возможность создания проекта модульного теста (которую мы вкратце рассматривали в главе 2, и которая продемонстрирована на рисунке 4-4).

Рисунок 4-4: Создание проекта модульного теста

Если вы выбрали опцию создания проекта модульного теста, то Visual Studio сгенерирует проект модульного теста при помощи Visual Studio Unit Testing Framework. Проект модульного теста содержит пару примерных тестов, которые можно обнаружить в классе HomeControllerTest, как это показано в листинге 4-8.

Примечание

Несмотря на то, что по умолчанию проект модульного теста использует Visual Studio Unit Testing Framework (MSTest), существует возможность расширения данного диалогового окна для того, чтобы воспользоваться другим фреймворком модульного тестирования, например, NUnit, MbUnit или xUnit.net. На практике для добавления других фреймворков тестирования проще воспользоваться NuGet, чем расширять это диалоговое окно.

Листинг 4-8: Примеры тестов для HomeController, предлагаемые по умолчанию
[TestClass]
public class HomeControllerTest
{
	[TestMethod]
	public void Index()
	{
		// Arrange
		HomeController controller = new HomeController();
		// Act
		ViewResult result = controller.Index() as ViewResult;
		// Assert
		Assert.AreEqual("Modify this template to jump-start", result.ViewBag.Message);
	}
	[TestMethod]
	public void About()
	{
		// Arrange
		HomeController controller = new HomeController();
		// Act
		ViewResult result = controller.About() as ViewResult;
		// Assert
		Assert.IsNotNull(result);
	}
}

Строка 8: Создает экземпляр контроллера

Строка 10: Тестирует метод действия

Строка 12: Выводит результаты

Эти предлагаемые по умолчанию тесты тестируют два метода действия, доступные по умолчанию в классе HomeController и создаваемые при создании новых MVC проектов.

Каждый тест имеет 3 фазы – arrange, act и assert. Первый тест создает экземпляр класса HomeController(это фаза arrange), вызывает метод Index этого экземпляра для извлечения экземпляра ViewResult, а затем посредством вызова статического метода Assert.AreEqual, сравнивающего сообщение во ViewBag с предполагаемым сообщением, указывает на то, что метод действия передал корректное сообщение во ViewBag. Тест для действия About даже проще, поскольку он всего лишь проверяет, что метод действия вернул ViewResult.

Если мы выполним эти тесты с помощью встроенного в Visual Studio инструмента запуска модульных тестов, то мы увидим, что оба эти теста выполнятся, как это показано на рисунке 4-5.

Рисунок 4-5: Выполнение модульных тестов MSTest в Visual Studio

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

Тестирование GuestbookController

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

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

Вместо прямого доступа к GuestbookContext мы могли бы ввести репозиторий, который предоставляет шлюз для выполнения операций доступа к данным наших GuestbookContext объектов. Начнем с создания интерфейса нашего репозитория:

public interface IGuestbookRepository
{
	IList<GuestbookEntry> GetMostRecentEntries();
	GuestbookEntry FindById(int id);
	IList<CommentSummary> GetCommentSummary();
	void AddEntry(GuestbookEntry entry);
}

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

Листинг 4-9: GuestbookRepository
public class GuestbookRepository : IGuestbookRepository
{
	private GuestbookContext _db = new GuestbookContext();
	public IList<GuestbookEntry> GetMostRecentEntries()
	{
		return (from entry in _db.Entries
						orderby entry.DateAdded descending
						select entry).Take(20).ToList();
	}
	public void AddEntry(GuestbookEntry entry)
	{
		entry.DateAdded = DateTime.Now;
		_db.Entries.Add(entry);
		_db.SaveChanges();
	}
	public GuestbookEntry FindById(int id)
	{
		var entry = _db.Entries.Find(id);
		return entry;
	}
	public IList<CommentSummary> GetCommentSummary()
	{
		var entries = from entry in _db.Entries
									group entry by entry.Name into groupedByName
									orderby groupedByName.Count() descending
									select new CommentSummary
									{
										NumberOfComments = groupedByName.Count(),
										UserName = groupedByName.Key
									};
		return entries.ToList();
	}
}

Строка 1: Реализует интерфейс

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

Листинг 4-10: Использование репозитория в GuestbookController
public class GuestbookController : Controller
{
	private IGuestbookRepository _repository;
	public GuestbookController()
	{
		_repository = new GuestbookRepository();
	}
	public GuestbookController(IGuestbookRepository repository)
	{
		_repository = repository;
	}
	public ActionResult Index()
	{
		var mostRecentEntries = _repository.GetMostRecentEntries();
		return View(mostRecentEntries);
	}
	public ActionResult Create()
	{
		return View();
	}
	[HttpPost]
	public ActionResult Create(GuestbookEntry entry)
	{
		if (ModelState.IsValid)
		{
			_repository.AddEntry(entry);
			return RedirectToAction("Index");
		}
		return View(entry);
	}
	public ViewResult Show(int id)
	{
		var entry = _repository.FindById(id);
		bool hasPermission = User.Identity.Name == entry.Name;
		ViewBag.HasPermission = hasPermission;
		return View(entry);
	}
	public ActionResult CommentSummary()
	{
		var entries = _repository.GetCommentSummary();
		return View(entries);
	}
}

Строка 3: Сохраняет репозиторий

Строка 6: Создает репозиторий по умолчанию

Строка 8: Предоставляет возможность вставки репозитория

Вместо создания экземпляра GuestbookContext теперь мы сохраняем экземпляр нашего репозитория в поле. Конструктор контроллера, используемый по умолчанию (который будет вызываться MVC фреймворком при запуске приложения), заполняет поле реализацией репозитория по умолчанию. У нас также есть второй конструктор, который дает нам возможность создать свой экземпляр репозитория вместо предлагаемого по умолчанию. Этот конструктор мы будем использовать в наших модульных тестах для передачи fake-реализации репозитория. Наконец, методы действий нашего контроллера теперь для доступа к данным используют репозиторий вместо выполнения LINQ запросов напрямую.

Примечание

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

Инъекция зависимостей

Технология передачи зависимостей в конструктор объекта известна как инъекция зависимостей (dependency injection). Как бы то ни было, мы выполнили инъекцию зависимостей вручную, добавив в наш класс разнообразные конструкторы. В главе 18 мы изучим, как можно использовать DI-контейнер, чтобы избежать необходимости добавления нескольких конструкторов. Более детальную информацию об инъекции зависимостей можно найти в книге "Dependency Injection in .NET" Марка Симанна (http://manning.com/seemann/) , а также в многочисленных online статьях таких, как “Inversion of Control Containers and the Dependency Injection Pattern” Мартина Фаулера (http://martinfowler.com/articles/injection.html).

На данном этапе мы можем тестировать наш контроллер отдельно от базы данных, но для достижения этого нам потребуется fake-реализация нашего интерфейса IGuestbookRepository, который не взаимодействует с базой данных. Существует несколько способов достижения данной цели – мы могли бы создать новый класс, который реализует этот интерфейс, но выполняет все операции в оперативной памяти (продемонстрировано в листинге 4-11), или мы могли бы использовать mock-фреймворк, например, Moq или Rhino Mocks (каждый из которых можно установить с помощью NuGet), для автоматического создания fake-реализации нашего интерфейса.

Листинг 4-11: Fake- реализация IGuestbookRepository
public class FakeGuestbookRepository : IGuestbookRepository
{
	private List<GuestbookEntry> _entries = new List<GuestbookEntry>();
	public IList<GuestbookEntry> GetMostRecentEntries()
	{
		return new List<GuestbookEntry>
			{
				new GuestbookEntry
				{
					DateAdded = new DateTime(2011, 6, 1),
					Id = 1,
					Message = "Test message",
					Name = "Jeremy"
				}
			};
	}
	public void AddEntry(GuestbookEntry entry)
	{
		_entries.Add(entry);
	}
	public GuestbookEntry FindById(int id)
	{
		return _entries.SingleOrDefault(x => x.Id == id);
	}
	public IList<CommentSummary> GetCommentSummary()
	{
		return new List<CommentSummary>
			{
				new CommentSummary
				{
					UserName = "Jeremy", NumberOfComments = 1
				}
			};
	}
}

Строка 3: Список, используемый для хранения

Fake-реализация нашего репозитория использует те же методы, что и реальный репозиторий, за исключением того что fake-репозиторий содержится в оперативной памяти, а методы GetCommentSummary и GetMostRecentEntries возвращают искусственные отклики (они всегда возвращают такие же fake- данные).

Поскольку наш контроллер содержит несколько методов действий, то существует несколько возможных тестов, которые мы могли написать. В следующем листинге показана пара тестов для метода Index.

Листинг 4-12: Тестирование метода Index
[TestMethod]
public void Index_RendersView()
{
	var controller = new GuestbookController(new FakeGuestbookRepository());
	var result = controller.Index() as ViewResult;
	Assert.IsNotNull(result);
}
[TestMethod]
public void Index_gets_most_recent_entries()
{
	var controller = new GuestbookController(new FakeGuestbookRepository());
	var result = (ViewResult)controller.Index();
	var guestbookEntries = (IList<GuestbookEntry>) result.Model;
	Assert.AreEqual(1, guestbookEntries.Count);
}

Строки 4, 11: Передает fake-репозиторий в контроллер

Первый из наших тестов вызывает метод Index и просто указывает на то, что он отображает представление (подобно тестам для класса HomeController). Второй тест более сложен – он указывает на то, что список GuestbookEntry объектов был передан в представление (если вы помните, метод действия Index вызывает метод GetMostRecentEntries нашего репозитория).

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

Модульное тестирование vs. TDD

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

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