ASP.NET MVC 4

ASP.NET MVC 4

Адам Фриман

Использование Moq

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

Один из хороших подходов заключается в использовании mock-объектов, которые симулируют функциональность реальных объектов проекта, но очень конкретным и контролируемым образом. Mock-объекты позволяют сузить фокус тестов, так чтобы вы могли проверить только тот функционал, в котором вы заинтересованы.

Платные версии Visual Studio 2012 включают в себя поддержку создания mock-объектов благодаря функции под названием fakes, но мы предпочитаем использовать библиотеку Moq, которая проста, удобна и может быть использована со всеми выпусками Visual Studio, в том числе бесплатными.

Понимание проблемы

Прежде чем начать использовать Moq мы хотим показать проблему, которые мы пытаемся исправить. В этом разделе мы собираемся провести модульное тестирование класса LinqValueCalculator, который мы определили в папке Models нашего проекта. В качестве напоминания мы представим в листинге 6-27 определение класса LinqValueCalculator.

Листинг 6-27: Класс LinqValueCalculator
using System.Collections.Generic;
using System.Linq;
namespace EssentialTools.Models
{
	public class LinqValueCalculator : IValueCalculator
	{
		private IDiscountHelper discounter;
		public LinqValueCalculator(IDiscountHelper discounterParam)
		{
			discounter = discounterParam;
		}
		public decimal ValueProducts(IEnumerable<Product> products)
		{
			return discounter.ApplyDiscount(products.Sum(p => p.Price));
		}
	}
}

Для тестирования этого класса мы добавили новый новый юнит тест класс в тестовый проект. Вы можете сделать это, щелкнув правой кнопкой мыши по тестовому проекту в Solution Explorer. Выберите пункт Add Unit Test из всплывающего меню. Если в меню Add нет пункта Unit Test, выберите New Item и используйте шаблон Basic Unit Test. Вы можете увидеть изменения, внесенные в новый файл, который Visual Studio по умолчанию называет UnitTest2.cs, в листинге 6-28.

Листинг 6-28: Добавление юнит теста для класса ShoppingCart
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using EssentialTools.Models;
using System.Linq;
namespace EssentialTools.Tests
{
	[TestClass]
	public class UnitTest2
	{
		private Product[] products = {
			new Product {Name = "Kayak", Category = "Watersports", Price = 275M},
			new Product {Name = "Lifejacket", Category = "Watersports", Price = 48.95M},
			new Product {Name = "Soccer ball", Category = "Soccer", Price = 19.50M},
			new Product {Name = "Corner flag", Category = "Soccer", Price = 34.95M}
		};
		[TestMethod]
		public void Sum_Products_Correctly()
		{
			// arrange
			var discounter = new MinimumDiscountHelper();
			var target = new LinqValueCalculator(discounter);
			var goalTotal = products.Sum(e => e.Price);
			// act
			var result = target.ValueProducts(products);
			// assert
			Assert.AreEqual(goalTotal, result);
		}
	}
}

Наша проблема состоит в том, что класс LinqValueCalculator зависит от реализации интерфейса IDiscountHelper. В этом примере мы использовали класс MinimumDiscountHelper, который заставляет нас подумать о двух вещах.

Во-первых, наш юнит тест стал сложным и негибким. Для того чтобы создать модульный тест, который работает, мы должны принять во внимание логику скидки в реализации IDiscountHelper, чтобы выяснить ожидаемое значение, возвращаемое методом ValueProducts. А хрупкость тестов заключается в том, что наши тесты не сработают, если в реализации изменится логика скидок.

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

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

Добавление Moq в проект Visual Studio

Так же как с Ninject ранее в этой главе, самый простой способ добавить Moq в проект MVC – это использовать интегрированную поддержку Visual Studio для NuGet, что позволяет легко установить широкий набор пакетов и без проблем их обновлять. Выберите Tools Library Package Manager Manage NuGet Packages for Solution, чтобы открыть диалоговое окно NuGet пакетов.

Выберите Online в левой панели и введите Moq в поле поиска в правом верхнем углу диалогового окна. Вы увидите ряд пакетов Moq, похожих на те, что показаны на рисунке 6-10.

Рисунок 6-10: Выбор Moq из пакетов NuGet

Нажмите кнопку Install для библиотеки Moq, и Visual Studio загрузит библиотеку и установит ее в ваш проект. Вы увидите Moq в разделе References проекта.

Внимание

Мы будем использовать Moq в проекте модульного тестирования, а не в MVC проекте, поэтому убедитесь, что вы добавить библиотеку в правильный проект.

Опять же, мы рекомендуем вам использовать NuGet, чтобы установить Moq, но мы загрузили библиотеку непосредственно с веб-сайта проекта (http://code.google.com/p/moq) и установили ее вручную, чтобы размер исходного кода, прилагаемого к этой книге, был минимизирован по максимуму. Мы установили ее вручную, выбрав Add Reference из Visual Studio меню Project, нажали на кнопку Browse, потом перешли к архиву, извлекли содержимое и выбрали файл Moq.dll.

Примечание

Мы хотим повторить то, что мы вам сказали, когда вручную устанавливали Ninject: единственная причина, почему мы добавили библиотеку Moq вручную, заключается в том, чтобы сохранить размер исходного кода, прилагаемого к этой книге. В реальных проектах мы используем NuGet, когда несколько дополнительных мегабайт данных не имеют никакого значения, и мы также рекомендуем вам использовать NuGet.

Добавление mock-объекта в юнит тест

Добавление mock-объекта в модульный тест обозначает, что вы говорите Moq, с каким объектом вы хотите работать, настраивая его поведение, а затем применяя объект к тестируемой цели. В листинге 6-29 показано, как мы добавили mock-объект в наш юнит тест для LinqValueCalculator.

Листинг 6-29: Использование mock-объекта в юнит тесте
using EssentialTools.Models;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using System.Linq;
namespace EssentialTools.Tests
{
	[TestClass]
	public class UnitTest2
	{
		private Product[] products = {
			new Product {Name = "Kayak", Category = "Watersports", Price = 275M},
			new Product {Name = "Lifejacket", Category = "Watersports", Price = 48.95M},
			new Product {Name = "Soccer ball", Category = "Soccer", Price = 19.50M},
			new Product {Name = "Corner flag", Category = "Soccer", Price = 34.95M}
		};
		[TestMethod]
		public void Sum_Products_Correctly()
		{
			// arrange
			Mock<IDiscountHelper> mock = new Mock<IDiscountHelper>();
			mock.Setup(m => m.ApplyDiscount(It.IsAny<decimal>()))
			.Returns<decimal>(total => total);
			var target = new LinqValueCalculator(mock.Object);
			// act
			var result = target.ValueProducts(products);
			// assert
			Assert.AreEqual(products.Sum(e => e.Price), result);
		}
	}
}

Синтаксис использования Moq немного странный, если вы впервые видите его, так что мы пройдем по каждой стадии процесса.

Совет

Имейте в виду, что существует целый ряд различных mock-библиотек, так что есть шанс, что вы можете найти альтернативу, если вам не нравится то, как работает Moq: хотя на самом деле Moq – легкая библиотека для использования. Есть другие популярные библиотеки, документация по которым составляет сотни страниц.

Создание mock-объекта

Первым делом надо сообщить Moq, с каким mock-объектом вы хотите работать. Moq в значительной степени зависит от параметров универсального типа, и вы видите это в том, как мы говорим Moq, что мы хотим создать mock-реализацию IDiscountHelper:

Mock<IDiscountHelper> mock = new Mock<IDiscountHelper>();

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

Выбор метода

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

mock.Setup(m => m.ApplyDiscount(It.IsAny<decimal>())).Returns<decimal>(total => total);

Мы используем метод Setup, чтобы добавить метод в наш mock-объект. Moq работ с использованием LINQ и лямбда-выражений. Когда мы вызываем метод Setup, Moq передает нам интерфейс, который мы попросили реализовать. В этом есть некая магия LINQ, в которую мы не собираемся углубляться, но она позволяет нам выбрать метод, который мы хотим определить, через лямбда-выражения. Для нашего модульного теста мы хотим определить поведение метода ApplyDiscount, который является единственным методом в интерфейсе IDiscountHelper и методом, которым нам нужно протестировать класс LinqValueCalculator.

Мы также должны сказать Moq, в каких значениях параметров мы заинтересованы, что мы и делаем, используя класс It, который мы выделили:

mock.Setup(m => m.ApplyDiscount(It.IsAny<decimal>())).Returns<decimal>(total => total);

Класс It определяет ряд методов, которые используются с параметрами универсального типа. В данном случае мы назвали метод IsAny, используя decimal как универсальный тип. Это говорит Moq, что поведение, которое мы определяем, следует применять всякий раз, когда вызывается ApplyDiscount с любым десятичным значением. В таблице 6-3 представлены методы класса It, и все они являются статическими.

Таблица 6-3: Методы класса It
Метод Описание
Is<T>(predicate) Задает значения типа T, которое заставляет предикат вернуть true (см. листинг 6-30 для примера).
IsAny<T>() Задает любое значение типа T.
IsInRange<T>(min, max, kind) Срабатывает, если параметр находится между определенными значениями и относится к типу T. Последний параметр является значением перечисления Range и может быть Inclusive или Exclusive.
IsRegex(expr) Соответствует строковому параметру, если он соответствует указанному регулярному выражению.

Мы покажем вам более сложный пример далее в этой главе, где используется другие It методы, но на данный момент мы рассмотрим метод IsAny<decimal>, который позволяет нам работать с любым десятичным значением.

Определение результата

Метод Returns позволяет определить результат, который будет возвращен, когда будет вызван наш mock-метод. Мы указали тип результата с помощью параметра типа и указали сам результат с помощью лямбда-выражения. Вот как мы это сделали:

mock.Setup(m => m.ApplyDiscount(It.IsAny<decimal>())).Returns<decimal>(total => total);

Вызывая метод Returns с параметром decimal (т.е. Returns<decimal>), мы говорим Moq, что собирается вернуть значение decimal. Для лямбда-выражения Moq передает нам значение типа, которое мы получили в методе ApplyDiscount: в этом примере мы создали переходный метод, в который мы возвращаем значение, переданное mock-методу ApplyDiscount без выполнения любых операций с ним. Это самый простой вид mock-метода, но в ближайшее время мы покажем вам более сложные примеры.

Использование mock-объекта

Последний шаг заключается в использовании mock-объекта в юнит тесте, что мы и делаем, считывая свойство Object объекта Mock<IDiscountHelper>:

var target = new LinqValueCalculator(mock.Object);

Итак, в нашем примере свойство Object возвращает реализацию интерфейса IDiscountHelper, где метод ApplyDiscount возвращает значение параметра decimal, который ему передается.

Наши тесты очень легко выполнить, потому что мы сами можем вывести цены объектов Product и убедиться, что мы получаем то же самое значение от объекта LinqValueCalculator:

Assert.AreEqual(products.Sum(e => e.Price), result);

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

Создание более сложного mock-объекта

Мы показали вам очень простой mock-объект в предыдущем разделе, но часть всей прелести Moq заключается в возможности быстро создавать сложные виды поведения для тестирования различных ситуаций. В листинге 6-30 мы добавили новый юнит тест в файл UnitTest2.cs, который представляет более сложную реализацию интерфейса IDiscountHelper: на самом деле, мы использовали Moq для моделирования поведения класса MinimumDiscountHelper.

Листинг 6-30: Использование Moq для класса MinimumDiscountHelper
using EssentialTools.Models;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using System.Linq;
namespace EssentialTools.Tests
{
	[TestClass]
	public class UnitTest2
	{
		private Product[] products = {
			new Product {Name = "Kayak", Category = "Watersports", Price = 275M},
			new Product {Name = "Lifejacket", Category = "Watersports", Price = 48.95M},
			new Product {Name = "Soccer ball", Category = "Soccer", Price = 19.50M},
			new Product {Name = "Corner flag", Category = "Soccer", Price = 34.95M}
		};
		[TestMethod]
		public void Sum_Products_Correctly()
		{
			// arrange
			Mock<IDiscountHelper> mock = new Mock<IDiscountHelper>();
			mock.Setup(m => m.ApplyDiscount(It.IsAny<decimal>()))
				.Returns<decimal>(total => total);
			var target = new LinqValueCalculator(mock.Object);
			// act
			var result = target.ValueProducts(products);
			// assert
			Assert.AreEqual(products.Sum(e => e.Price), result);
		}
		private Product[] createProduct(decimal value)
		{
			return new[] { new Product { Price = value } };
		}
		[TestMethod]
		[ExpectedException(typeof(System.ArgumentOutOfRangeException))]
		public void Pass_Through_Variable_Discounts()
		{
			// arrange
			Mock<IDiscountHelper> mock = new Mock<IDiscountHelper>();
			mock.Setup(m => m.ApplyDiscount(It.IsAny<decimal>()))
				.Returns<decimal>(total => total);
			mock.Setup(m => m.ApplyDiscount(It.Is<decimal>(v => v == 0)))
				.Throws<System.ArgumentOutOfRangeException>();
			mock.Setup(m => m.ApplyDiscount(It.Is<decimal>(v => v > 100)))
				.Returns<decimal>(total => (total * 0.9M));
			mock.Setup(m => m.ApplyDiscount(It.IsInRange<decimal>(10, 100, Range.Inclusive)))
				.Returns<decimal>(total => total - 5);
			var target = new LinqValueCalculator(mock.Object);
			// act
			decimal FiveDollarDiscount = target.ValueProducts(createProduct(5));
			decimal TenDollarDiscount = target.ValueProducts(createProduct(10));
			decimal FiftyDollarDiscount = target.ValueProducts(createProduct(50));
			decimal HundredDollarDiscount = target.ValueProducts(createProduct(100));
			decimal FiveHundredDollarDiscount = target.ValueProducts(createProduct(500));
			// assert
			Assert.AreEqual(5, FiveDollarDiscount, "$5 Fail");
			Assert.AreEqual(5, TenDollarDiscount, "$10 Fail");
			Assert.AreEqual(45, FiftyDollarDiscount, "$50 Fail");
			Assert.AreEqual(95, HundredDollarDiscount, "$100 Fail");
			Assert.AreEqual(450, FiveHundredDollarDiscount, "$500 Fail");
			target.ValueProducts(createProduct(0));
		}
	}
}

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

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

mock.Setup(m => m.ApplyDiscount(It.IsAny<decimal>())).Returns<decimal>(total => total);

Это то же самое поведение, которое было использовано в предыдущем примере, и мы включили его сюда, потому что порядок, в котором вы вызываете метод Setup, влияет на поведение mock-объекта. Moq оценивает данные ему виды поведения в обратном порядке, так что самые последние вызовы метода Setup считаются первыми. Это обозначает, что вы должны создавать mock-поведения в порядке от более общих к более конкретным. Условие It.IsAny<decimal> является наиболее общим условием, которое мы определили в данном примере, и поэтому мы применяем его первым. Если мы изменим порядок вызовов Setup, такое поведение может охватить все вызовы метода ApplyDiscount и сгенерировать неправильный результат.

Использование mock-объектов для указанных значений (и получение исключения)

Для нашего второго вызова метод Setup мы использовали метод It.Is:

mock.Setup(m => m.ApplyDiscount(It.Is<decimal>(v => v == 0)))
	.Throws<System.ArgumentOutOfRangeException>();

Предикат, который мы передали методу Is, возвращает true, если значение, переданное методу ApplyDiscount равно 0. Вместо того чтобы вернуть результат, мы использовали метод Throws, который заставляет Moq выбросить новый экземпляр исключения, которое мы указываем с параметром типа.

Мы также используем метод Is, чтобы охватить значения больше, чем 100:

mock.Setup(m => m.ApplyDiscount(It.Is<decimal>(v => v > 100)))
.Returns<decimal>(total => (total * 0.9M));

Использование метода It.Is – это наиболее гибкий способ создания определенного поведения для различных значений параметра, потому что вы можете использовать любой предикат, который возвращает true или false. Этот метод мы используем чаще всего при создании сложных mock-объектов.

Использование mock-объектов для диапазона значений

Наше последнее использование объекта It связано с методом IsInRange, который позволяет нам охватить определенный диапазон значений параметров:

mock.Setup(m => m.ApplyDiscount(It.IsInRange<decimal>(10, 100, Range.Inclusive)))
	.Returns<decimal>(total => total - 5);

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

mock.Setup(m => m.ApplyDiscount(It.Is<decimal>(v => v >= 10 && v <= 100)))
	.Returns<decimal>(total => total - 5);

Результат тот же, но мы считаем, что работа через предикат отличается более гибким подходом. Moq обладает целым рядом чрезвычайно полезных функций, и вы можете ознакомиться с ними на http://code.google.com/p/moq/wiki/QuickStart.

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