ASP.NET MVC 4

ASP.NET MVC 4

Адам Фриман

Модульное тестирование при помощи Visual Studio

Есть много .NET пакетов для модульного тестирования, многие из которых имеют открытый исходный код и находятся в свободном доступе. В этой книге мы будем использовать встроенную поддержку модульного тестирования, которая поставляется с Visual Studio, но есть и другие доступные .NET пакеты для модульного тестирования. Наиболее популярным является, вероятно, NUnit, но все остальные пакеты для юнит тестирования делают фактически то же самое. Причина, почему мы выбрали поддержку юнит тестирования Visual Studio, заключается в том, что нам нравится интеграция с остальной частью IDE. Хотя с Visual Studio 2012 Microsoft дал возможность интегрировать сторонние библиотеки тестирования в IDE, чтобы вы могли работать с ними так же, как и со встроенными инструментами тестирования.

Чтобы показать поддержку модульного тестирования Visual Studio, мы собираемся добавить новую реализацию интерфейса IDiscountHelper в проект для примера. Создайте новый файл MinimumDiscountHelper.cs в папке Models. Убедитесь, что содержание соответствуют показанному в листинге 6-22.

Листинг 6-22: Содержание файла MinumumDiscountHelper.cs
using System;
namespace EssentialTools.Models
{
	public class MinimumDiscountHelper : IDiscountHelper
	{
		public decimal ApplyDiscount(decimal totalParam)
		{
			throw new NotImplementedException();
		}
	}
}

Наша цель в данном примере заключается в том, чтобы MinimumDiscountHelper показал следующее поведение:

  • Если общая сумма превышает $100, скидка составит 10 процентов.
  • Если общая сумма составляет от $10 до $100 включительно, скидка будет $5.
  • Для общей суммы меньше $10 скидки не будет.
  • Если общая сумма является отрицательным числом, выбрасывается исключение ArgumentOutOfRangeException.

Наш класс MinimumDiscountHelper не реализует пока еще ни одной из этих форм поведения: мы собираемся следовать подходу Test Driven Development (TDD), когда сначала пишутся юнит тесты, а только затем создается код.

Создание проекта по юнит тестированию

Первый шаг, который нам нужно сделать, это создать проект для модульного тестирования. Щелкните правой кнопкой мыши по Solution 'EssentialTools' нашего проекта в Solution Explorer и выберите Add New Project из всплывающего меню.

Совет

Вы можете создать проект по тестированию, когда вы создаете новый MVC проект: в диалоговом окне, где вы выбираете начальный контент для MVC проекта, есть опция Create a unit test project.

Рисунок 6-5: Создание юнит тест проекта

Назовите проект EssentialTools.Tests и нажмите кнопку ОК, чтобы создать новый проект, который будет добавлен к текущему решению Visual Studio вместе с проектом MVC приложения.

Нам нужно добавить ссылку на тестовый проект, чтобы мы могли использовать его для выполнения тестов по классам MVC проекта. Щелкните правой кнопкой мыши в Solution Explorer по References для проекта EssentialTools.Tests, а затем выберите Add Reference из всплывающего меню. Нажмите Solution в левой панели и поставьте рядом с EssentialTools, как показано на рисунке 6-6.

Рисунок 6-6: Добавление ссылки в MVC проект

Создание юнит тестов

Мы добавим наши юнит тесты в файл UnitTest1.cs проекта EssentialTools.Tests. В платных выпусках Visual Studio есть некоторые полезные функции для автоматической генерации тестовых методов, которые не доступны в Visual Studio Express, но мы все же можем создавать полезные и хорошие тесты (и наш опыт по автоматически генерируемым тестам был довольно успешный). Чтобы начать работу, мы внесли изменения, показанные в листинге 6-23.

Листинг 6-23: Добавление тестовых методов в файл UnitTest1.cs
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using EssentialTools.Models;
namespace EssentialTools.Tests
{
	[TestClass]
	public class UnitTest1
	{
		private IDiscountHelper getTestObject()
		{
			return new MinimumDiscountHelper();
		}
		[TestMethod]
		public void Discount_Above_100()
		{
			// arrange
			IDiscountHelper target = getTestObject();
			decimal total = 200;
			// act
			var discountedTotal = target.ApplyDiscount(total);
			// assert
			Assert.AreEqual(total * 0.9M, discountedTotal);
		}
	}
}

Мы только что добавили один юнит тест. Класс, который содержит тесты, помечается атрибутом TestClass, а отдельными тестами являются методы, отмеченные атрибутом TestMethod. Не все методы в классе модульных тестов должны быть юнит тестами. Чтобы показать это, мы определили метод getTestObject, который мы будем использовать, чтобы регулировать наши тесты. Поскольку этот метод не имеет атрибута TestMethod, Visual Studio не будет обрабатывать его как юнит тест.

Совет

Обратите внимание, что мы должны были добавить выражение using, чтобы импортировать пространство имен EssentialTools.Models в тестовый класс. Тестовые классы не отличаются от обычных C# классов: только атрибуты TestClass и TestMethod добавляют магию тестирования в проект.

Вы видите, что мы следовали паттерну arrange/act/assert (A/A/A) в методе модульного теста. Есть бесчисленное множество соглашений о том, как называть юнит тесты, но мы считаем, что нужно просто использовать имена, которые дают понять, что проверяет тест. Наш метод модульного теста называется Discount_Above_100, что является для нас понятным и имеющим смысл. Но на самом деле, важно, чтобы вы (и ваша команда) понимали, на каком соглашении стоит остановиться, если вы хотите принять другую схему имен, отличную от нашей.

Сперва мы вызвали метод getTestObject. Он создает экземпляр объекта, который мы собираемся тестировать: в данном случае это класса MinimumDiscountHelper. Мы также определяем значение total, которое мы собираемся протестировать. Это часть arrange нашего модульного теста.

Для части act нашего теста мы вызываем метод MinimumDiscountHelper.ApplyDiscount и присваиваем результат переменной discountedTotal. Наконец, для части assert теста мы используем метод Assert.AreEqual, чтобы проверить, что значение, которые мы получили от метода ApplyDiscount, составляет 90% от первоначальной общей стоимости.

Класс Assert имеет ряд статических методов, которые вы можете использовать в своих тестах. Этот класс можно найти в пространстве имен Microsoft.VisualStudio.TestTools.UnitTesting наряду с некоторыми дополнительными классами, которые могут быть полезны для создания и проведения тестов. Вы можете узнать больше о классах в данном пространстве имен на http://msdn.microsoft.com/en-us/library/ms182530.aspx.

Мы наиболее часто используем класс Assert, и мы привели наиболее важные методы в таблице 6-2 (хотя их гораздо больше, и их стоит изучить).

Таблица 6-2: Статические методы класса Assert
Метод Описание
AreEqual<T>(T, T), AreEqual<T>(T, T, string) Утверждает, что два объекта типа T имеют одинаковое значение.
AreNotEqual<T>(T, T), AreNotEqual<T>(T, T, string) Утверждает, что два объекта типа T не имеют одинакового значения.
AreSame<T>(T, T), AreSame<T>(T, T, string) Утверждает, что две переменные относятся к одному объекту.
AreNotSame<T>(T, T), AreNotSame<T>(T, T, string) Утверждает, что две переменные относятся к разным объектам.
Fail(), Fail(string) Утверждение не выполнилось: условия не проверены.
Inconclusive(), Inconclusive(string) Указывает, что результат модульного теста не может быть окончательно установлен.
IsTrue(bool), IsTrue(bool, string) Утверждает, что значение bool верно: чаще всего используется для оценки выражения, возвращающего булев результат.
IsFalse(bool), IsFalse(bool, string) Утверждает, что значение bool ложно.
IsNull(object), IsNull(object, string) Утверждает, что переменной не присвоена ссылка на объект.
IsNotNull(object), IsNotNull(object, string) Утверждает, что переменной присвоена ссылка на объект.
IsInstanceOfType(object, Type), IsInstanceOfType(object, Type, string) Утверждает, что это объект указанного типа или унаследован от указанного типа.
IsNotInstanceOfType(object, Type), IsNotInstanceOfType(object, Type, string) Утверждает, что этот объект не является объектом указанного типа.

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

Каждый из этих методов имеет перегруженную версию, которая принимает параметр string. Строка включена в качестве сообщения об исключении, если утверждение не выполняется. Методы AreEqual и AreNotEqual имеют ряд перегруженных версий, которые предназначены для сопоставления конкретных типов. Например, есть версия, которая позволяет сравнивать строки, не принимая во внимание регистр.

Совет

Одним из примечательных членов пространства имен Microsoft.VisualStudio.TestTools.UnitTesting является атрибут ExpectedException. Это утверждение, которое будет выполнено только в том случае, если модульный тест выбросит исключение типа, указанного параметром ExceptionType. Это аккуратный способ ловить исключения без необходимости возиться с блоками try...catch в юнит тесте.

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

Листинг 6-24: Определение оставшихся тестов
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using EssentialTools.Models;
namespace EssentialTools.Tests
{
	[TestClass]
	public class UnitTest1
	{
		private IDiscountHelper getTestObject()
		{
			return new MinimumDiscountHelper();
		}
		[TestMethod]
		public void Discount_Above_100()
		{
			// arrange
			IDiscountHelper target = getTestObject();
			decimal total = 200;
			// act
			var discountedTotal = target.ApplyDiscount(total);
			// assert
			Assert.AreEqual(total * 0.9M, discountedTotal);
		}
		[TestMethod]
		public void Discount_Between_10_And_100()
		{
			//arrange
			IDiscountHelper target = getTestObject();
			// act
			decimal TenDollarDiscount = target.ApplyDiscount(10);
			decimal HundredDollarDiscount = target.ApplyDiscount(100);
			decimal FiftyDollarDiscount = target.ApplyDiscount(50);
			// assert
			Assert.AreEqual(5, TenDollarDiscount, "$10 discount is wrong");
			Assert.AreEqual(95, HundredDollarDiscount, "$100 discount is wrong");
			Assert.AreEqual(45, FiftyDollarDiscount, "$50 discount is wrong");
		}
		[TestMethod]
		public void Discount_Less_Than_10()
		{
			//arrange
			IDiscountHelper target = getTestObject();
			// act
			decimal discount5 = target.ApplyDiscount(5);
			decimal discount0 = target.ApplyDiscount(0);
			// assert
			Assert.AreEqual(5, discount5);
			Assert.AreEqual(0, discount0);
		}
		[TestMethod]
		[ExpectedException(typeof(ArgumentOutOfRangeException))]
		public void Discount_Negative_Total()
		{
			//arrange
			IDiscountHelper target = getTestObject();
			// act
			target.ApplyDiscount(-1);
		}
	}
}

Запуск юнит тестов (и они не срабатывают)

Visual Studio 2012 представила более полезное окно Test Explorer для управления и запуска тестов. Выберите Windows Test Explorer из меню Test Visual Studio, чтобы увидеть новое окно, и нажмите кнопку Run All в верхнем левом углу. Вы увидите результаты, похожие на те, что показаны на рисунке 6-7.

Рисунок 6-7: Запуск тестов для проекта

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

Реализация тестов

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

Листинг 6-25: Реализация класса MinimumDiscountHelper
using System;
namespace EssentialTools.Models
{
	public class MinimumDiscountHelper : IDiscountHelper
	{
		public decimal ApplyDiscount(decimal totalParam)
		{
			if (totalParam < 0)
			{
				throw new ArgumentOutOfRangeException();
			}
			else if (totalParam > 100)
			{
				return totalParam * 0.9M;
			}
			else if (totalParam > 10 && totalParam <= 100)
			{
				return totalParam - 5;
			}
			else
			{
				return totalParam;
			}
		}
	}
}

Тестирование и исправление кода

Мы сознательно оставили ошибку в этом коде, чтобы продемонстрировать, как работает повторяющееся модульное тестирование в Visual Studio. Вы можете увидеть результат ошибки, если нажмете кнопку Run All в окне Test Explorer. Результаты тестов показаны на рисунке 6-8.

Рисунок 6-8: Ошибка при запуске юнит тестов

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

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

И теперь мы возвращаемся к нашему коду и видим, что мы не совсем правильно реализовали ожидаемое поведение: в частности, скидки для сумм от 10 до 100 не правильно обрабатываются. Проблема лежит в этом выражении класса MinumumDiscountHelper:

...
} else if (totalParam > 10 && totalParam < 100) {
...

В нашей системе скидок мы должны задать поведение для сумм, которые находятся в диапазоне от $10 до $100 включительно, но наша реализация исключает эти значения и проверяет только те значения, которые больше, чем $10, а ровно $10 не проверяет. Решение тут простое, и оно показано в листинге 6-26: должен быть добавлен только один символ, чтобы изменить результат оператора if:

Листинг 6-26: Исправление кода
using System;
namespace EssentialTools.Models
{
	public class MinimumDiscountHelper : IDiscountHelper
	{
		public decimal ApplyDiscount(decimal totalParam)
		{
			if (totalParam < 0)
			{
				throw new ArgumentOutOfRangeException();
			}
			else if (totalParam > 100)
			{
				return totalParam * 0.9M;
			}
			else if (totalParam >= 10 && totalParam <= 100)
			{
				return totalParam - 5;
			}
			else
			{
				return totalParam;
			}
		}
	}
}

Если мы нажмем на кнопку Run All в окне Test Explorer, результаты покажут, что мы решили проблему и что наш код прошел все тесты (см. рисунок 6-9).

Рисунок 6-9: Прохождение всех тестов

Мы кратко показали вам модульное тестирование, но мы также будем работать с юнит тестами в следующих главах. Поддержка модульного тестирования в Visual Studio довольно хорошо реализована, и мы рекомендуем вам изучить документацию по модульному тестированию в MSDN. Эту документацию который вы можете найти на http://msdn.microsoft.com/enus/library/dd264975.aspx.

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