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

ASP_NET_MVC_4_Framework_s_primerami_na_C_dlya_p

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

}

public class DefaultDiscountHelper : IDiscountHelper

{

public decimal ApplyDiscount(decimal totalParam)

{

return (totalParam - (10m / 100m * totalParam));

}

}

}

IDiscountHelper определяет метод ApplyDiscount, который будет применять скидку к значению decimal. Класс DefaultDiscounterHelper реализует интерфейс и применяет фиксированную 10процентную скидку. Мы изменили класс LinqValueCalculator, так что он использует интерфейс IDiscountHelper при выполнении расчетов, как показано в листинге 6-14.

Листинг 6-14: Добавление зависимости в класс LinqValueCalculator

using System.Collections.Generic; using System.Linq;

namespace EssentialTools.Models

{

public class LinqValueCalculator : IValueCalculator

{

private IDiscountHelper discounter;

public LinqValueCalculator(IDiscountHelper discountParam)

{

discounter = discountParam;

}

public decimal ValueProducts(IEnumerable<Product> products)

{

return discounter.ApplyDiscount(products.Sum(p => p.Price));

}

}

}

Вновь добавленный конструктор класса принимает реализацию интерфейса IDiscountHelper, которая затем используется в методе ValueProducts, чтобы применить скидку к совокупной стоимости обрабатываемых объектов Product.

Мы связываем интерфейс IDiscountHelper c классом реализации, используя ядро Ninject в классе

NinjectDependencyResolver, так же как мы сделали для IValueCalculator, как показано в листинге

6-15.

Листинг 6-15: Связывание другого интерфейса с его реализацией

using System;

using System.Collections.Generic; using System.Web.Mvc;

using Ninject;

using Ninject.Parameters; using Ninject.Syntax;

using System.Configuration; using EssentialTools.Models;

namespace EssentialTools.Infrastructure

{

public class NinjectDependencyResolver : IDependencyResolver

{

private IKernel kernel;

public NinjectDependencyResolver()

{

kernel = new StandardKernel(); AddBindings();

131

}

public object GetService(Type serviceType)

{

return kernel.TryGet(serviceType);

}

public IEnumerable<object> GetServices(Type serviceType)

{

return kernel.GetAll(serviceType);

}

private void AddBindings()

{

kernel.Bind<IValueCalculator>().To<LinqValueCalculator>(); kernel.Bind<IDiscountHelper>().To<DefaultDiscountHelper>();

}

}

}

Мы создали цепочку зависимостей, которую легко обрабатывает Ninject, используя связи, которые мы определили в пользовательском DR. Для того чтобы удовлетворить запрос для класса HomeController, Ninject считает, что необходимо создать реализацию для класса IValueCalculator,

ион делает это, рассматривая свои связи и понимая, что наша политика для этого интерфейса заключается в использовании класса LinqValueCalculator. Но для того, чтобы создать объект LinqValueCalculator, Ninject понимает, что он должен использовать реализацию IDiscountHelper,

ипоэтому он рассматривает связи и создает объект DefaultDiscountHelper. Он создает DefaultDiscountHelper и передает его конструктору объекта LinqValueCalculator, который в свою

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

Определение значений свойств и параметров конструктора

Мы можем конфигурировать классы, которые создает Ninject, путем предоставления информации о нужных значениях, применяемых к свойствам, когда мы связываем интерфейс с его реализацией. Чтобы продемонстрировать эту функцию, мы изменили класс DefaultDiscountHelper так, что он определяет свойство DiscountSize, которое используется для расчета суммы скидки, как показано в листинге 6-16.

Листинг 6-16: Добавление свойства в класс реализации

namespace EssentialTools.Models

{

public interface IDiscountHelper

{

decimal ApplyDiscount(decimal totalParam);

}

public class DefaultDiscountHelper : IDiscountHelper

{

public decimal DiscountSize { get; set; } public decimal ApplyDiscount(decimal totalParam)

{

return (totalParam - (DiscountSize / 100m * totalParam));

}

}

}

Когда мы привязываем конкретный класс к типу при помощи Ninject, мы можем использовать метод WithPropertyValue для установки значения свойства DiscountSize в классе

132

DefaultDiscountHelper. Вы можете увидеть изменение, которое мы внесли в метод AddBindings

класса NinjectDependencyResolver, в листинге 6-17.

Листинг 6-17: Использование Ninject метода WithPropertyValue

private void AddBindings() { kernel.Bind<IValueCalculator>().To<LinqValueCalculator>(); kernel.Bind<IDiscountHelper>()

.To<DefaultDiscountHelper>().WithPropertyValue("DiscountSize", 50M);

}

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

Значение свойства установлено в связке с классом DefaultDiscountHelper, и его результат заключается в вычислении общей стоимости предметов со скидкой. Это изменение показано на рисунке 6-4.

Рисунок 6-4: Результат применения скидки через свойство при работе с цепочкой зависимостей

Если вам нужно установить более одного значения свойства, можно составить цепочку вызовов метода WithPropertyValue и охватить их всех. Мы можем сделать то же самое с параметрами конструктора. В листинге 6-18 показан класс DefaultDiscounterHelper, переделанный таким образом, чтобы размер скидки передавался в качестве параметра конструктора.

Листинг 6-18: Использование свойства конструктора в классе реализации

namespace EssentialTools.Models

{

public interface IDiscountHelper

{

decimal ApplyDiscount(decimal totalParam);

}

public class DefaultDiscountHelper : IDiscountHelper

{

public decimal discountSize;

public DefaultDiscountHelper(decimal discountParam)

{

discountSize = discountParam;

}

public decimal ApplyDiscount(decimal totalParam)

{

return (totalParam - (discountSize / 100m * totalParam));

}

}

}

Чтобы связать этот класс, используя Ninject, мы указываем значение параметра конструктора при помощи метода WithConstructorArgument в методе AddBindings, как показано в листинге 6-19.

133

Листинг 6-19: Связывание с классом, который требует параметр конструктора

private void AddBindings() { kernel.Bind<IValueCalculator>().To<LinqValueCalculator>(); kernel.Bind<IDiscountHelper>()

.To<DefaultDiscountHelper>().WithConstructorArgument("discountParam", 50M);

}

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

Совет

Обратите внимание, что мы не просто изменили вызов WithPropertyValue к WithConstructorArgument. Мы также изменили имя нужного члена, чтобы он

соответствовал соглашению C# по именам параметров.

Использование условной связки

Ninject поддерживает ряд условных связующих методов, которые позволяют нам указать, какие классы должны использоваться для ответов на запросы для конкретных запросов. Чтобы продемонстрировать эту функцию, мы добавили новый файл FlexibleDiscountHelper.cs в папку Models нашего проекта, содержимое которого вы можете увидеть в листинге 6-20.

Листинг 6-20: Содержание файла FlexibleDiscountHelper.cs

namespace EssentialTools.Models

{

public class FlexibleDiscountHelper : IDiscountHelper

{

public decimal ApplyDiscount(decimal totalParam)

{

decimal discount = totalParam > 100 ? 70 : 25; return (totalParam - (discount / 100m * totalParam));

}

}

}

Класс FlexibleDiscountHelper применяет различные скидки, исходя из величины общей суммы, на которую распространяется скидка. Затем мы можем изменить метод AddBindings в NinjectDependencyResolver, чтобы сказать Ninject, когда мы хотим использовать

FlexibleDiscountHelper и когда мы хотим использовать DefaultDiscountHelper, как показано в листинге 6-21.

Листинг 6-21: Обновление метода AddBindings для использования условной связки

private void AddBindings() { kernel.Bind<IValueCalculator>().To<LinqValueCalculator>(); kernel.Bind<IDiscountHelper>()

.To<DefaultDiscountHelper>().WithConstructorArgument("discountParam", 50M); kernel.Bind<IDiscountHelper>().To<FlexibleDiscountHelper>()

.WhenInjectedInto<LinqValueCalculator>();

}

Новая связка указывает, что если класс FlexibleDiscountHelper должен использоваться в качестве реализации интерфейса IDiscountHelper, тогда Ninject будет внедрять реализацию в объект

LinqValueCalculator.

134

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

Таблица 6-1: Условные связующие методы Ninject

Метод

 

Результат

 

 

 

When(predicate)

 

Связка используется, если утверждение (predicate) – лямбда-выражение –

 

считается верным.

 

 

Связка используется, если внедряемый класс, аннотируется атрибутом, чей

WhenClassHas<T>()

тип определяется Т.

WhenInjectedInto<T>()Связка используется, если внедряемый класс принадлежит к типу T.

Модульное тестирование при помощи 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 скидки не будет.

135

Если общая сумма является отрицательным числом, выбрасывается исключение

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.

136

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

}

}

}

137

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

Утверждает, что два объекта типа T имеют

string)

одинаковое значение.

 

 

,

Утверждает, что два объекта типа T не имеют

AreNotEqual<T>(T, T) AreNotEqual<T>(T, T,

одинакового значения.

string)

 

 

AreSame<T>(T, T), AreSame<T>(T, T, string)

Утверждает, что две переменные относятся к

одному объекту.

 

 

AreNotSame<T>(T, T), AreNotSame<T>(T, T,

Утверждает, что две переменные относятся к

string)

разным объектам.

 

 

Fail(), Fail(string)

Утверждение не выполнилось: условия не

проверены.

138

Метод

Inconclusive(), Inconclusive(string)

IsTrue(bool), IsTrue(bool, string)

IsFalse(bool), IsFalse(bool, string)

IsNull(object), IsNull(object, string)

IsNotNull(object), IsNotNull(object, string)

IsInstanceOfType(object, Type), IsInstanceOfType(object, Type, string)

IsNotInstanceOfType(object, Type), IsNotInstanceOfType(object, Type, string)

Описание

Указывает, что результат модульного теста не может быть окончательно установлен.

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

Утверждает, что значение bool ложно.

Утверждает, что переменной не присвоена ссылка на объект.

Утверждает, что переменной присвоена ссылка на объект.

Утверждает, что это объект указанного типа или унаследован от указанного типа.

Утверждает, что этот объект не является объектом указанного типа.

Каждый из статических методов класса 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();

139

}

[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.

140

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