Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

ASP_NET_MVC_4_Framework_s_primerami_na_C_dlya_p

.pdf
Скачиваний:
29
Добавлен:
19.03.2016
Размер:
17.66 Mб
Скачать

Рисунок 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

{

141

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

142

{

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.

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

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

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

143

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()

{

144

// 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

145

Нажмите кнопку 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);

146

// 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);

147

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

Таблица 6-3: Методы класса It

Метод

Is<T>(predicate)

IsAny<T>()

IsInRange<T>(min, max, kind)

Описание

Задает значения типа T, которое заставляет предикат вернуть true (см. листинг 6-30 для примера).

Задает любое значение типа T.

Срабатывает, если параметр находится между определенными значениями и относится к типу 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);

148

Преимущество использования 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)))

149

.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 – это наиболее гибкий способ создания определенного поведения для различных значений параметра, потому что вы можете использовать любой предикат, который

150

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]