ASP.NET MVC Урок E. Тестирование

от автора

Цель урока. Научиться создавать тесты для кода. NUnit. Принцип применения TDD. Mock. Юнит-тесты. Интегрированное тестирование. Генерация данных.

Тестирование, принцип TDD, юнит-тестирование и прочее.

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

  1. Мы делаем сайт, показываем заказчику, он высылает список неточностей и дополнительных пожеланий, мы их бодро правим и сайт отдаем заказчику, т.е. выкладываем на его сервер. На его сервер никто не ходит, заказчик понимает, что чуда не произошло и перестает платить за хостинг/домен. Сайт умирает. Нужны ли там тесты?
  2. Мы делаем сайт, показываем заказчику, он высылает список правок, мы их бодро правим, запускаем сайт. Через полгода на сайте 300 уников в день и эта цифра растет изо дня в день. Заказчик постоянно просит новые фичи, старый код начинает разрастаться, и со временем его всё сложнее поддерживать.

Видите ли, тут дилемма такова, что результат запуска сайта непредсказуемый. У меня было и так, что сделанный на коленке сайт запустился весьма бодро, а бывало, что крутую работу заказчик оплатил, но даже не принял. Итак, тактика поведения может быть такой:

  • Писать тесты всегда. Мы крутая компания, мы покрываем 90% кода всяческими тестами, и нам реально всё равно, что мы тратим на это в 100500 раз больше времени/денег, ведь результат полностью предсказуем и мы вообще красавцы.
  • Не писать тесты никогда. Мы крутая компания, мы настолько идеально работаем, что можем в уме пересобрать весь проект, и если наш код компилируется, это значит, что он полностью рабочий. Если что-то не работает, то вероятно это хостинг, или ошибка в браузере. или фича такая.
  • Писать тесты, но не всегда. Тут мы должны понять, что каким бы ни был сайт или проект, то он состоит из функционала. А это значит, что пользователям должны быть предоставлены всяческие возможности, и возможности важные, я бы даже сказал — критические, как-то зарегистрироваться на сайте, сделать заказ, добавить новость или комментарий. Неприятно, когда хочешь, а не можешь зарегистрироваться, ведь сайт-то нужный.
  • Для чего используются тесты? Это как принцип ведения двойной записи в бухгалтерии. Каждое действие, каждый функционал проверяется не только работоспособностью сайта, но и еще как минимум одним тестом. При изменении кода юнит-тесты указывают, что имнно пошло не так и красным подсвечивают места, где произошло нарушение. Но так ли это?

Рассмотрим принцип TDD:

  1. Прочитать задание и написать тест, который заваливается
  2. Написать любой код, который позволяет проходить данный тест и остальные тесты
  3. Сделать рефакторинг, т.е. убрать повторяющийся код, если надо, но чтобы все тесты проходили

Например, было дано следующее исправление:

Мы решили добавить в блог поле тегов. Так как у нас уже существует много записей в блоге, то это поле решили сделать необязательным. Так как уже есть существующий код, то скаффолдингом не пользовались. Вручную проверили создание записи – всё ок. Прогнали тесты – всё ок. Но забыли добавить изменение поля в UpdatePost (cache.Tags = instance.Tags;). При изменении старой записи мы добавляем теги, которые собственно не сохраняются. При этом тесты прошли на ура. Жизнь — боль!

Что ж, как видно, мы нарушили основной принцип TDD – вначале пиши тест, который заваливается, а уже потом пиши код, который его обрабатывает. Но(!) тут есть и вторая хитрость — мы написали тест, который проверяет создание записи блога с тегом. Конечно, сразу же у нас это не скомпилировалось (т.е. тест не прошел), но мы добавили в ModelView что-то типа throw new NotImplementedException(). Всё скомпилировалось, тест горит красным, мы добавляем это поле с тегом, убирая исключение, тест проходит. Все остальные тесты тоже проходят. Принципы соблюдены, а ошибка осталась.

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

  • Добавление информации
  • Проверка информации
  • Изменение информации
  • Удаление информации
  • Проверка прав на действие
  • Выдача информации

Это основные действия. Как, например, проходит регистрация:

  • Показываем поля для заполнения
  • При нажатии на «Зарегистрироваться» проверяем данные
  • Если всё удачно, то выдаем страничку «Молодец», если же не всё хорошо, то выдаем предупреждение и позволяем исправить оплошность
  • Если всё хорошо, то в БД у нас появляется запись
  • А еще мы письмо с активацией отправляем

Создадим для всего этого юнит-тесты:

  • Что мы показываем ему поля для заполнения (т.е. передаем пустой объект класса RegisterUserView)
  • Что у нас стоят атрибуты и всё такое, проверяем, что действительно ли мы проверяем, что можно записать в БД
  • Что выдаем именно «Молодец» страницу
  • Что появляется запись, что было две записи, а стало три записи
  • Что пытаемся что-то отправить, находим шаблон и вызвываем MailNotify.

Приступим, пожалуй.

Установить NUnit

Идем по ссылке http://sourceforge.net/projects/nunit/ и устанавливаем NUnit. Так же в VS устанавливаем NUnit Test Adapter (ну чтобы запускать тесты прямо в VS).


Создадим папочку типа Solution Folder Test и в нее добавим проект LessonProject.UnitTest и установим там NUnit:

Install-Package NUnit 

Создадим класс UserControllerTest в (/Test/Default/UserContoller.cs):

  [TestFixture]     public class UserControllerTest     {     } 

Итак, принцип написания наименования методов тестов Method_Scenario_ExpectedBehavior:

  • Method – метод [или свойство], который тестируем
  • Scenario – сценарий, который мы тестируем
  • ExpectedBehavior – ожидаемое поведение

Например, проверяем первое, что возвращаем View c классом UserView для регистрации:

 public void Register_GetView_ItsOkViewModelIsUserView()         {             Console.WriteLine("=====INIT======");             var controller = new UserController();             Console.WriteLine("======ACT======");             var result = controller.Register();             Console.WriteLine("====ASSERT=====");             Assert.IsInstanceOf<ViewResult>(result);             Assert.IsInstanceOf<UserView>(((ViewResult)result).Model);  } 

Итак, все тесты делятся на 3 части Init->Act->Assert:

  • Init – инициализация, мы получаем наш UserController
  • Act – действие, мы запускаем наш controller.Register
  • Assert – проверка, что всё действительно так.

Откроем вкладку Test Explorer:

Если адаптер NUnit правильно был установлен, то мы увидим наш тест-метод.
Запускаем. Тест пройден, можно идти открывать шампанское. Стоооп. Это лишь самая легкая часть, а как быть с той частью, где мы что-то сохраняем. В данном случае мы не имеем БД, наш Repositary – null, ноль, ничего.
Изучим теперь класс и методы для инициализации (документация). SetUpFixture – класс, помеченный этим атрибутом, означает, что в нем есть методы, которые проводят инициализацию перед тестами и зачистку после тестов. Это относится к одному и тому же пространству имен.

  • Setup – метод, помеченный этим атрибутом, вызывается до выполнения всех тестовых методов. Если находится в классе с атрибутом TestFixture, то вызывается перед выполнением методов только этого класса.
  • TearDown – метод, помеченный этим атрибутом, вызывается после выполнения всех тестов. Если находится в классе с атрибутом TestFixture, то вызывается после выполнения всех методов.

Создадим класс UnitTestSetupFixture.cs (/Setup/UnitTestSetupFixture.cs):

  [SetUpFixture]     public class UnitTestSetupFixture     {         [SetUp]         public void Setup()         {             Console.WriteLine("===============");             Console.WriteLine("=====START=====");             Console.WriteLine("===============");         }          [TearDown]         public void TearDown()         {             Console.WriteLine("===============");             Console.WriteLine("=====BYE!======");             Console.WriteLine("===============");         }     } 

Запустим и получим:
=============== =====START===== =============== =====INIT====== ======ACT====== ====ASSERT===== =============== =====BYE!====== ===============

Mock

Итак, Mock – это объект-пародия. Т.е. например, не база данных, а что-то похожее на базу данных. Мираж, в общем-то. Есть еще Stub – это заглушка. Пример метода заглушки:

public int GetRandom()         {             return 4;         } 

Но мы будем использовать Mock:

Install-Package Moq 

Определим, какое окружение есть у нас, чтобы мы проинициализировали для него Mock-объекты. В принципе, это всё, что мы некогда вынесли в Ninject Kernel:

  • IRepository
  • IConfig
  • IMapper
  • IAuthentication

И тут я сделаю небольшое замечание. Мы не можем вынести Config в объекты-миражи. Не в плане, что это совсем невозможно, а в плане – что это плохая затея. Например, мы изменили шаблон письма так, что string.Format() выдает ошибку FormatException. А в тесте всё хорошо, тест отлично проходит. И за что он после этого отвечает? Ни за что. Так что файл конфигурации надо использовать оригинальный. Оставим это на потом.

По поводу, IMapper – в этом нет необходимости, мы совершенно спокойно можем использовать и CommonMapper.
Но для начала проинициазируем IKernel для работы в тестовом режиме. В App_Start/NinjectWebCommon.cs мы в методе RegisterServices указываем, как должны быть реализованы интерфейсы, и вызываем это в bootstrapper.Initialize(CreateKernel). В дальнейшем мы обращаемся по поводу получения сервиса через DependencyResolver.GetService(). Так что создадим NinjectDependencyResolver (/Tools/NinjectDependencyResolver.cs):

public class NinjectDependencyResolver : IDependencyResolver     {         private readonly IKernel _kernel;          public NinjectDependencyResolver(IKernel kernel)         {             _kernel = kernel;         }          public object GetService(Type serviceType)         {             return _kernel.TryGet(serviceType);         }          public IEnumerable<object> GetServices(Type serviceType)         {             try             {                 return _kernel.GetAll(serviceType);             }             catch (Exception)             {                 return new List<object>();             }         }     } 

Добавим в SetUp метод (/Setup/UnitTestSetupFixture.cs):

[SetUp]         public virtual void Setup()         {  		InitKernel(); } protected virtual IKernel InitKernel()          {             var kernel = new StandardKernel();             DependencyResolver.SetResolver(new NinjectDependencyResolver(kernel));      InitRepository(kernel); //потом сделаем             return kernel;         }  

Создадим MockRepository
(/Mock/Repository/MockRepository.cs):

public partial class MockRepository : Mock<IRepository>     {         public MockRepository(MockBehavior mockBehavior = MockBehavior.Strict)             : base(mockBehavior)         {             GenerateRoles();             GenerateLanguages();             GenerateUsers();                      }     } 

(/Mock/Repository/Entity/Language.cs)

namespace LessonProject.UnitTest.Mock {     public partial class MockRepository     {         public List<Language> Languages { get; set; }           public void GenerateLanguages()         {             Languages = new List<Language>();             Languages.Add(new Language()             {                 ID = 1,                 Code = "en",                 Name = "English"             });             Languages.Add(new Language()             {                 ID = 2,                 Code = "ru",                 Name = "Русский"             });             this.Setup(p => p.Languages).Returns(Languages.AsQueryable());         }     } } 

(/Mock/Repository/Entity/Role.cs)

    public partial class MockRepository     {         public List<Role> Roles { get; set; }          public void GenerateRoles()         {             Roles = new List<Role>();             Roles.Add(new Role()             {                 ID = 1,                 Code = "admin",                 Name = "Administrator"             });              this.Setup(p => p.Roles).Returns(Roles.AsQueryable());         }     } 

(/Mock/Repository/Entity/User.cs)

public partial class MockRepository     {         public List<User> Users { get; set; }          public void GenerateUsers()         {             Users = new List<User>();              var admin = new User()             {                 ID = 1,                 ActivatedDate = DateTime.Now,                 ActivatedLink = "",                 Email = "admin",                 FirstName = "",                 LastName = "",                 Password = "password",                 LastVisitDate = DateTime.Now,             };              var role = Roles.First(p => p.Code == "admin");             var userRole = new UserRole()             {                 User = admin,                 UserID = admin.ID,                 Role = role,                 RoleID = role.ID             };              admin.UserRoles =                  new EntitySet<UserRole>() {                     userRole                 };             Users.Add(admin);              Users.Add(new User()             {                 ID = 2,                 ActivatedDate = DateTime.Now,                 ActivatedLink = "",                 Email = "chernikov@gmail.com",                 FirstName = "Andrey",                 LastName = "Chernikov",                 Password = "password2",                 LastVisitDate = DateTime.Now             });              this.Setup(p => p.Users).Returns(Users.AsQueryable());             this.Setup(p => p.GetUser(It.IsAny<string>())).Returns((string email) =>                  Users.FirstOrDefault(p => string.Compare(p.Email, email, 0) == 0));      this.Setup(p => p.Login(It.IsAny<string>(), It.IsAny<string>())).Returns((string email, string password) =>                 Users.FirstOrDefault(p => string.Compare(p.Email, email, 0) == 0));         }     } 

Рассмотрим, как работает Mock. У него есть такой хороший метод, как Setup (опять?! сплошной сетап!), который работает таким образом:
this.Setup(что у нас запрашивают).Returns(что мы отвечаем на это);

Например:
this.Setup(p => p.WillYou()).Returns(true);

Рассмотрим подробнее, какие еще могут быть варианты:

  • Методы
    var mock = new Mock<IFoo>(); mock.Setup(foo => foo.DoSomething("ping")).Returns(true); 
    • параметр out
      var outString = "ack"; mock.Setup(foo => foo.TryParse("ping", out outString)).Returns(true); 

    • ссылочный параметр
      var instance = new Bar(); mock.Setup(foo => foo.Submit(ref instance)).Returns(true); 

    • зависимость от входного параметра и возвращаемого значения (можно и несколько параметров)
      mock.Setup(x => x.DoSomething(It.IsAny<string>()))                 .Returns((string s) => s.ToLower()); 

    • кидаем исключение
      mock.Setup(foo => foo.DoSomething("reset")).Throws<InvalidOperationException>(); mock.Setup(foo => foo.DoSomething("")).Throws(new ArgumentException("command"); 

    • возвращает различные значения для (???) и использование Callback
      var mock = new Mock<IFoo>(); var calls = 0; mock.Setup(foo => foo.GetCountThing())     .Returns(() => calls)     .Callback(() => calls++); 

  • Соответсвие на аргументы
    • любое значение
      mock.Setup(foo => foo.DoSomething(It.IsAny<string>())).Returns(true); 

    • условие через Func<bool, T>
      mock.Setup(foo => foo.Add(It.Is<int>(i => i % 2 == 0))).Returns(true); 

    • нахождение в диапазоне
      mock.Setup(foo => foo.Add(It.IsInRange<int>(0, 10, Range.Inclusive))).Returns(true); 

    • Regex выражение
      mock.Setup(x => x.DoSomething(It.IsRegex("[a-d]+", RegexOptions.IgnoreCase))).Returns("foo"); 

  • Свойства
    • Любое свойство
      mock.Setup(foo => foo.Name).Returns("bar"); 

    • Любой иерархии свойство
      mock.Setup(foo => foo.Bar.Baz.Name).Returns("baz"); 

  • Обратные вызовы (callback)
    • Без параметров
      mock.Setup(foo => foo.Execute("ping"))                 .Returns(true)                 .Callback(() => calls++); 

    • С параметром
           mock.Setup(foo => foo.Execute(It.IsAny<string>()))                 .Returns(true)                 .Callback((string s) => calls.Add(s)); 

    • С параметром, немного другой синтаксис
                  mock.Setup(foo => foo.Execute(It.IsAny<string>()))                 .Returns(true)                 .Callback<string>(s => calls.Add(s)); 

      Несколько параметров

           mock.Setup(foo => foo.Execute(It.IsAny<int>(), It.IsAny<string>()))                 .Returns(true)                 .Callback<int, string>((i, s) => calls.Add(s)); 

      До и после вызова

                  mock.Setup(foo => foo.Execute("ping"))                 .Callback(() => Console.WriteLine("Before returns"))                 .Returns(true)                 .Callback(() => Console.WriteLine("After returns")); 

    Проверка (Mock объект сохраняет количество обращений к своим параметрам, тем самым мы также можем проверить правильно ли был исполнен код)

    • Обычная проверка, что был вызван метод Execute с параметром “ping”
      mock.Verify(foo => foo.Execute("ping")); 

    • С добавлением собственного сообщения об ошибке
           mock.Verify(foo => foo.Execute("ping"), "When doing operation X, the service should be pinged always"); 

    • Не должен был быть вызван ни разу
      mock.Verify(foo => foo.Execute("ping"), Times.Never()); 

    • Хотя бы раз должен был быть вызван
      mock.Verify(foo => foo.Execute("ping"), Times.AtLeastOnce()); mock.VerifyGet(foo => foo.Name); 
    • Должен был быть вызван именно сеттер для свойства
                  mock.VerifySet(foo => foo.Name); 

    • Должен был быть вызван сеттер со значением “foo”
            mock.VerifySet(foo => foo.Name = "foo"); 

    • Сеттер должен был быть вызван со значением в заданном диапазоне
      mock.VerifySet(foo => foo.Value = It.IsInRange(1, 5, Range.Inclusive)); 

    Хорошо, этого нам пока хватит, остальное можно будет почитать здесь:
    https://code.google.com/p/moq/wiki/QuickStart

    Возвращаемся обратно в UnitTestSetupFixture.cs (/Setup/UnitTestSetupFixture.cs) и инициализируем конфиг:

    protected virtual void InitRepository(StandardKernel kernel)         {             kernel.Bind<MockRepository>().To<MockRepository>().InThreadScope();             kernel.Bind<IRepository>().ToMethod(p => kernel.Get<MockRepository>().Object);         } 

    Проверим какой-то наш вывод, например класс /Default/Controllers/UserController:cs:

    [Test]         public void Index_GetPageableDataOfUsers_CountOfUsersIsTwo()         {             //init             var controller = DependencyResolver.Current.GetService<Areas.Default.Controllers.UserController>();             //act             var result = controller.Index();              Assert.IsInstanceOf<ViewResult>(result);             Assert.IsInstanceOf<PageableData<User>>(((ViewResult)result).Model);             var count = ((PageableData<User>)((ViewResult)result).Model).List.Count();              Assert.AreEqual(2, count);         } 

    В BaseController.cs (/LessonProject/Controllers/BaseController.cs) уберем атрибуты Inject у свойств Auth и Config (иначе выделенная строка не сможет проинициализовать контроллер и вернет null). Кстати о выделенной строке. Мы делаем именно такую инициализацию, чтобы все Inject-атрибутованные свойства были проинициализированы. Запускаем, и, правда, count == 2. Отлично, MockRepository работает. Вернем назад атрибуты Inject.
    Кстати, тесты не запускаются обычно в дебаг-режиме, чтобы запустить Debug надо сделать так:
    Теперь поработаем с Config. Это будет круто!

    TestConfig

    Что нам нужно сделать. Нам нужно:

    • Взять Web.Config c проекта LessonProject (каким-то хитрым образом)
    • И на его базе создать некий класс, который будет реализовывать IConfig интерфейс
    • Ну и поцепить на Ninject Kernel
    • И можно использовать.

    Начнем. Для того чтобы взять Web.Config – нам нужно скопировать его в свою папку. Назовем её Sandbox. Теперь скопируем, поставим на pre-build Event в Project Properties:

    xcopy $(SolutionDir)LessonProject\Web.config $(ProjectDir)Sandbox\ /y 

    При каждом запуске билда мы копируем Web.config (и, если надо, то перезаписываем) к себе в Sandbox.
    Создадим TestConfig.cs и в конструктор будем передавать наш файл (/Tools/TestConfig.cs):

    public class TestConfig : IConfig     {         private Configuration configuration;          public TestConfig(string configPath)         {             var configFileMap = new ExeConfigurationFileMap();             configFileMap.ExeConfigFilename = configPath;             configuration = ConfigurationManager.OpenMappedExeConfiguration(configFileMap, ConfigurationUserLevel.None);         }           public string ConnectionStrings(string connectionString)         {             return configuration.ConnectionStrings.ConnectionStrings[connectionString].ConnectionString;         }          public string Lang         {             get             {                 return configuration.AppSettings.Settings["Lang"].Value;             }         }          public bool EnableMail         {             get             {                 return bool.Parse(configuration.AppSettings.Settings["EnableMail"].Value);             }         }          public IQueryable<IconSize> IconSizes         {             get             {                 IconSizesConfigSection configInfo = (IconSizesConfigSection)configuration.GetSection("iconConfig");                 if (configInfo != null)                 {                     return configInfo.IconSizes.OfType<IconSize>().AsQueryable<IconSize>();                 }                 return null;             }         }          public IQueryable<MimeType> MimeTypes         {             get             {                 MimeTypesConfigSection configInfo = (MimeTypesConfigSection)configuration.GetSection("mimeConfig");                 return configInfo.MimeTypes.OfType<MimeType>().AsQueryable<MimeType>();             }         }          public IQueryable<MailTemplate> MailTemplates         {             get {                 MailTemplateConfigSection configInfo = (MailTemplateConfigSection)configuration.GetSection("mailTemplatesConfig");                 return configInfo.MailTemplates.OfType<MailTemplate>().AsQueryable<MailTemplate>();              }         }          public MailSetting MailSetting         {             get             {                 return (MailSetting)configuration.GetSection("mailConfig");             }         }          public SmsSetting SmsSetting         {             get              {                 return (SmsSetting)configuration.GetSection("smsConfig");             }         }     } 

    И инициализируем в UnitTestSetupFixture.cs (/Setup/UnitTestSetupFixture.cs):

     protected virtual void InitConfig(StandardKernel kernel)         {             var fullPath = new FileInfo(Sandbox + "/Web.config").FullName;             kernel.Bind<IConfig>().ToMethod(c => new TestConfig(fullPath));         } 


    Создадим простой тест на проверку данных в конфиге:

    [TestFixture]     public class MailTemplateTest     {         [Test]         public void MailTemplates_ExistRegisterTemplate_Exist()         {             var config = DependencyResolver.Current.GetService<IConfig>();             var template = config.MailTemplates.FirstOrDefault(p => p.Name.StartsWith("Register"));             Assert.IsNotNull(template);         }     } 

    Запускаем, проверяем, вуаля! Переходим к реализации IAuthentication.

    Authentication

    В веб-приложении, когда мы уже исполняем код в контроллере, мы уже имеем какой-то заданный контекст, окружение, сформированное http-запросом. Т.е. это и параметры, и кукисы, и данные о версии браузера, и каково разрешение экрана, и какая операционная система. В общем, это всё – HttpContext. Следует понимать, что мы при авторизации помещаем в кукисы какие-то данные, а потом достаем их и всё. Собственно, для этого мы создадим специальный интерфейс IAuthCookieProvider, который будет типа записывать кукисы
    IAuthCookieProvider.cs (LessonProject/Global/Auth/IAuthCookieProvider):

    public interface IAuthCookieProvider     {         HttpCookie GetCookie(string cookieName);          void SetCookie(HttpCookie cookie);     } 

    И реализуем его для HttpAuthCookieProvider.cs (/Global/Auth/HttpAuthCookieProvider.cs):

    public class HttpContextCookieProvider : IAuthCookieProvider     {         public HttpContextCookieProvider(HttpContext HttpContext)         {             this.HttpContext = HttpContext;         }          protected HttpContext HttpContext { get; set; }          public HttpCookie GetCookie(string cookieName)         {             return HttpContext.Request.Cookies.Get(cookieName);         }          public void SetCookie(HttpCookie cookie)         {             HttpContext.Response.Cookies.Set(cookie);         }     } 

    И теперь используем эту реализацию для работы с Cookies в CustomAuthentication (/Global/Auth/CustomAuthentication.cs):

    public IAuthCookieProvider AuthCookieProvider { get; set; } 

    и вместо HttpContext.Request.Cookies.Get – используем GetCookie() и
    HttpContext.Response.Cookies.Set – соответственно SetCookie().
    Изменяем и в IAuthencation.cs (/Global/Auth/IAuthencation.cs):

     public interface IAuthentication     {         /// <summary>         /// Конекст (тут мы получаем доступ к запросу и кукисам)         /// </summary>         IAuthCookieProvider AuthCookieProvider { get; set; }  

    И в AuthHttpModule.cs (/Global/Auth/AuthHttpModule.cs):

    var auth = DependencyResolver.Current.GetService<IAuthentication>();       auth.AuthCookieProvider = new HttpContextCookieProvider(context); 
    MockHttpContext

    Теперь создадим Mock-объекты для HttpContext в LessonProject.UnitTest:

    MockHttpContext.cs в (/Mock/HttpContext.cs): public class MockHttpContext : Mock<HttpContextBase>     {         [Inject]         public HttpCookieCollection Cookies { get; set; }          public MockHttpCachePolicy Cache { get; set; }          public MockHttpBrowserCapabilities Browser { get; set; }          public MockHttpSessionState SessionState { get; set; }          public MockHttpServerUtility ServerUtility { get; set; }          public MockHttpResponse Response { get; set; }          public MockHttpRequest Request { get; set; }          public MockHttpContext(MockBehavior mockBehavior = MockBehavior.Strict)             : this(null, mockBehavior)         {         }          public MockHttpContext(IAuthentication auth, MockBehavior mockBehavior = MockBehavior.Strict)             : base(mockBehavior)         {             //request              Browser = new MockHttpBrowserCapabilities(mockBehavior);             Browser.Setup(b => b.IsMobileDevice).Returns(false);              Request = new MockHttpRequest(mockBehavior);             Request.Setup(r => r.Cookies).Returns(Cookies);             Request.Setup(r => r.ValidateInput());             Request.Setup(r => r.UserAgent).Returns("Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.64 Safari/537.11");             Request.Setup(r => r.Browser).Returns(Browser.Object);             this.Setup(p => p.Request).Returns(Request.Object);              //response             Cache = new MockHttpCachePolicy(MockBehavior.Loose);                         Response = new MockHttpResponse(mockBehavior);             Response.Setup(r => r.Cookies).Returns(Cookies);             Response.Setup(r => r.Cache).Returns(Cache.Object);             this.Setup(p => p.Response).Returns(Response.Object);              //user             if (auth != null)             {                 this.Setup(p => p.User).Returns(() => auth.CurrentUser);             }             else             {                 this.Setup(p => p.User).Returns(new UserProvider("", null));             }              //Session State             SessionState = new MockHttpSessionState();             this.Setup(p => p.Session).Returns(SessionState.Object);              //Server Utility             ServerUtility = new MockHttpServerUtility(mockBehavior);             this.Setup(p => p.Server).Returns(ServerUtility.Object);              //Items             var items = new ListDictionary();             this.Setup(p => p.Items).Returns(items);         }     } 

    Кроме этого создаем еще такие классы:

    • MockHttpCachePolicy
    • MockHttpBrowserCapabilities
    • MockHttpSessionState
    • MockHttpServerUtility
    • MockHttpResponse
    • MockHttpRequest

    Все эти mock-объекты весьма тривиальны, кроме MockSessionState, где и хранится session-storage (/Mock/Http/MockHttpSessionState.cs):

    public class MockHttpSessionState : Mock<HttpSessionStateBase>     {         Dictionary<string, object> sessionStorage;           public MockHttpSessionState(MockBehavior mockBehavior = MockBehavior.Strict)             : base(mockBehavior)         {             sessionStorage = new Dictionary<string, object>();             this.Setup(p => p[It.IsAny<string>()]).Returns((string index) => sessionStorage[index]);             this.Setup(p => p.Add(It.IsAny<string>(), It.IsAny<object>())).Callback<string, object>((name, obj) =>             {                 if (!sessionStorage.ContainsKey(name))                 {                     sessionStorage.Add(name, obj);                 }                 else                 {                     sessionStorage[name] = obj;                 }             });         }     } 

    Создаем FakeAuthCookieProvider.cs (/Fake/FakeAuthCookieProvider.cs):

     public class FakeAuthCookieProvider : IAuthCookieProvider     {         [Inject]         public HttpCookieCollection Cookies { get; set; }          public HttpCookie GetCookie(string cookieName)         {             return Cookies.Get(cookieName);         }          public void SetCookie(HttpCookie cookie)         {             if (Cookies.Get(cookie.Name) != null)             {                 Cookies.Remove(cookie.Name);             }             Cookies.Add(cookie);         }     } 

    Фух! Инициализируем это в UnitTestSetupFixture.cs (/Setup/UnitTestSetupFixture.cs):

    protected virtual void InitAuth(StandardKernel kernel)         {             kernel.Bind<HttpCookieCollection>().To<HttpCookieCollection>();             kernel.Bind<IAuthCookieProvider>().To<FakeAuthCookieProvider>().InSingletonScope();             kernel.Bind<IAuthentication>().ToMethod<CustomAuthentication>(c =>             {                 var auth = new CustomAuthentication();                 auth.AuthCookieProvider = kernel.Get<IAuthCookieProvider>();                 return auth;             });         } 

    Заметим, что Bind происходит на SingletonScope(), т.е. единожды авторизовавшись в каком-то тесте, мы в последующих тестах будем использовать эту же авторизацию.

    Компилим и пытаемся с этим всем взлететь. Сейчас начнется магия…

    Проверка валидации

    Если мы просто вызовем что-то типа:

    var registerUser = new UserView()             {                 Email = "user@sample.com",                 Password = "123456",                 ConfirmPassword = "1234567",                 AvatarPath = "/file/no-image.jpg",                 BirthdateDay = 1,                 BirthdateMonth = 12,                 BirthdateYear = 1987,                 Captcha = "1234"             };             var result = controller.Register(registerUser); 

    То, во-первых, никакая неявная валидация не выполнится, а во-вторых, у нас там есть session и мы ее не проинициализировали, она null и всё – ошибка. Так что проверку валидации (та, что в атрибутах) будем устраивать через отдельный класс. Назовем его Валидатор Валидаторович (/Tools/Validator.cs):

    public class ValidatorException : Exception     {         public ValidationAttribute Attribute { get; private set; }          public ValidatorException(ValidationException ex, ValidationAttribute attribute)             : base(attribute.GetType().Name, ex)         {             Attribute = attribute;         }     }      public class Validator     {         public static void ValidateObject<T>(T obj)         {             var type = typeof(T);             var meta = type.GetCustomAttributes(false).OfType<MetadataTypeAttribute>().FirstOrDefault();             if (meta != null)             {                 type = meta.MetadataClassType;             }              var typeAttributes = type.GetCustomAttributes(typeof(ValidationAttribute), true).OfType<ValidationAttribute>();             var validationContext = new ValidationContext(obj);             foreach (var attribute in typeAttributes)             {                 try                 {                     attribute.Validate(obj, validationContext);                 }                 catch (ValidationException ex)                 {                     throw new ValidatorException(ex, attribute);                 }             }              var propertyInfo = type.GetProperties();             foreach (var info in propertyInfo)             {                 var attributes = info.GetCustomAttributes(typeof(ValidationAttribute), true).OfType<ValidationAttribute>();                 foreach (var attribute in attributes)                 {                     var objPropInfo = obj.GetType().GetProperty(info.Name);                     try                     {                         attribute.Validate(objPropInfo.GetValue(obj, null), validationContext);                     }                     catch (ValidationException ex)                     {                         throw new ValidatorException(ex, attribute);                     }                 }             }         }     } 

    Итак, что тут у нас происходит. Вначале мы получаем все атрибуты класса T, которые относятся к типу ValidationAttribute:

    var typeAttributes = type.GetCustomAttributes(typeof(ValidationAttribute), true).OfType<ValidationAttribute>();             var validationContext = new ValidationContext(obj);             foreach (var attribute in typeAttributes)             {                 try                 {                     attribute.Validate(obj, validationContext);                 }                 catch (ValidationException ex)                 {                     throw new ValidatorException(ex, attribute);                 }             } 

    Потом аналогично для каждого свойства:

    var propertyInfo = type.GetProperties();             foreach (var info in propertyInfo)             {                 var attributes = info.GetCustomAttributes(typeof(ValidationAttribute), true).OfType<ValidationAttribute>();                 foreach (var attribute in attributes)                 {                     var objPropInfo = obj.GetType().GetProperty(info.Name);                     try                     {                         attribute.Validate(objPropInfo.GetValue(obj, null), validationContext);                     }                     catch (ValidationException ex)                     {                         throw new ValidatorException(ex, attribute);                     }                 }             } 

    Если валидация не проходит, то происходит исключение, и мы оборачиваем его в ValidatorException, передавая еще и атрибут, по которому произошло исключение.
    Теперь по поводу капчи и Session. Мы должны контроллеру передать контекст (MockHttpContext):

    var controller = DependencyResolver.Current.GetService<Areas.Default.Controllers.UserController>();             var httpContext = new MockHttpContext().Object;             ControllerContext context = new ControllerContext(new RequestContext(httpContext,  new RouteData()), controller);             controller.ControllerContext = context;             controller.Session.Add(CaptchaImage.CaptchaValueKey, "1111"); 

    И теперь всё вместе:

    [Test]         public void Index_RegisterUserWithDifferentPassword_ExceptionCompare()         {             //init             var controller = DependencyResolver.Current.GetService<Areas.Default.Controllers.UserController>();             var httpContext = new MockHttpContext().Object;             ControllerContext context = new ControllerContext(new RequestContext(httpContext,  new RouteData()), controller);             controller.ControllerContext = context;              //act             var registerUserView = new UserView()             {                 Email = "user@sample.com",                 Password = "123456",                 ConfirmPassword = "1234567",                 AvatarPath = "/file/no-image.jpg",                 BirthdateDay = 1,                 BirthdateMonth = 12,                 BirthdateYear = 1987,                 Captcha = "1111"             };             try             {                 Validator.ValidateObject<UserView>(registerUserView);             }             catch (Exception ex)             {                 Assert.IsInstanceOf<ValidatorException>(ex);                 Assert.IsInstanceOf<System.ComponentModel.DataAnnotations.CompareAttribute>(((ValidatorException)ex).Attribute);             }         }  

    Запускаем, и всё получилось. Но капча проверяется непосредственно в методе контроллера. Специально для капчи:

      [Test]         public void Index_RegisterUserWithWrongCaptcha_ModelStateWithError()         {             //init             var controller = DependencyResolver.Current.GetService<Areas.Default.Controllers.UserController>();             var httpContext = new MockHttpContext().Object;             ControllerContext context = new ControllerContext(new RequestContext(httpContext, new RouteData()), controller);             controller.ControllerContext = context;             controller.Session.Add(CaptchaImage.CaptchaValueKey, "2222");             //act             var registerUserView = new UserView()             {                 Email = "user@sample.com",                 Password = "123456",                 ConfirmPassword = "1234567",                 AvatarPath = "/file/no-image.jpg",                 BirthdateDay = 1,                 BirthdateMonth = 12,                 BirthdateYear = 1987,                 Captcha = "1111"             };              var result = controller.Register(registerUserView);             Assert.AreEqual("Текст с картинки введен неверно", controller.ModelState["Captcha"].Errors[0].ErrorMessage);         }  

    Круто!

    Проверка авторизации

    Например, мы должны проверить, что, если я захожу не под админом, то в авторизованную часть (в контроллер, помеченный атрибутом [Authorize(Roles=“admin”)]) – обычному польвателю не дадут войти. Есть отличный способ это проверить. Обратим внимание на класс ControllerActionInvoker и отнаследуем его для вызовов (/Fake/FakeControllerActionInvoker.cs + FakeValueProvider.cs):

    public class FakeValueProvider     {         protected Dictionary<string, object> Values { get; set; }          public FakeValueProvider()         {             Values = new Dictionary<string, object>();         }          public object this[string index]          {             get              {                 if (Values.ContainsKey(index))                 {                     return Values[index];                 }                 return null;             }              set             {                 if (Values.ContainsKey(index))                 {                     Values[index] = value;                 }                 else                 {                     Values.Add(index, value);                 }             }         }     }   public class FakeControllerActionInvoker<TExpectedResult> : ControllerActionInvoker where TExpectedResult : ActionResult     {         protected FakeValueProvider FakeValueProvider { get; set; }          public FakeControllerActionInvoker()         {             FakeValueProvider = new FakeValueProvider();         }          public FakeControllerActionInvoker(FakeValueProvider fakeValueProvider)         {             FakeValueProvider = fakeValueProvider;         }          protected override ActionExecutedContext InvokeActionMethodWithFilters(ControllerContext controllerContext, IList<IActionFilter> filters, ActionDescriptor actionDescriptor, IDictionary<string, object> parameters)         {             return base.InvokeActionMethodWithFilters(controllerContext, filters, actionDescriptor, parameters);         }          protected override object GetParameterValue(ControllerContext controllerContext, ParameterDescriptor parameterDescriptor)         {             var obj = FakeValueProvider[parameterDescriptor.ParameterName];             if (obj != null)             {                 return obj;             }             return parameterDescriptor.DefaultValue;         }          protected override void InvokeActionResult(ControllerContext controllerContext, ActionResult actionResult)         {             Assert.IsInstanceOf<TExpectedResult>(actionResult);         }     }  

    По сути это «вызывальщик» action-методов контроллеров, где Generic класс – это ожидаемый класс результата. В случае неавторизации это будет HttpUnauthorizedResult. Сделаем тест (/Test/Admin/HomeControllerTest.cs):

    [TestFixture]     public class AdminHomeControllerTest     {         [Test]         public void Index_NotAuthorizeGetDefaultView_RedirectToLoginPage()         {             var auth = DependencyResolver.Current.GetService<IAuthentication>();             auth.Login("chernikov@gmail.com", "password2", false);              var httpContext = new MockHttpContext(auth).Object;             var controller = DependencyResolver.Current.GetService<Areas.Admin.Controllers.HomeController>();             var route = new RouteData();             route.Values.Add("controller", "Home");             route.Values.Add("action", "Index");             route.Values.Add("area", "Admin");              ControllerContext context = new ControllerContext(new RequestContext(httpContext, route), controller);             controller.ControllerContext = context;              var controllerActionInvoker = new FakeControllerActionInvoker<HttpUnauthorizedResult>();             var result = controllerActionInvoker.InvokeAction(controller.ControllerContext, "Index");         }      } 

    Запускаем тест, он проходит. Сделаем, чтобы авторизация была под пользователем admin и будем ожидать получение ViewResult:

    [Test]         public void Index_AdminAuthorize_GetViewResult()         {             var auth = DependencyResolver.Current.GetService<IAuthentication>();             auth.Login("admin", "password", false);              var httpContext = new MockHttpContext(auth).Object;             var controller = DependencyResolver.Current.GetService<Areas.Admin.Controllers.HomeController>();             var route = new RouteData();             route.Values.Add("controller", "Home");             route.Values.Add("action", "Index");             route.Values.Add("area", "Admin");              ControllerContext context = new ControllerContext(new RequestContext(httpContext, route), controller);             controller.ControllerContext = context;              var controllerActionInvoker = new FakeControllerActionInvoker<ViewResult>();             var result = controllerActionInvoker.InvokeAction(controller.ControllerContext, "Index");         }  

    Так же прошли. Молодцом.

    На этом давайте остановимся и подумаем, чего мы достигли. Мы можем оттестировать любой контроллер, проверить правильность любой валидации, проверку прав пользователя. Но это касается только контроллера. А как же работа с моделью? Да, мы можем проверить, что вызывается метод репозитория, но на этом всё. Да, мы можем написать Mock-методы для добавления, изменения, удаления, но как это поможет решить ту проблему, о которой я писал вначале главы? Как мы заметим, что что-то не так при упущении поля с тегом? В хрестоматийном примере NerdDinner тесты не покрывают эту область.

    Есть IRepository, есть SqlRepository, есть MockRepository. И всё что находится в SqlRepository – это не покрытая тестами область. А там может быть реализовано очень многое. Что же делать? К чему этот TDD?

    Интегрированное тестирование

    Идея будет совершенно безумной, мы будем использовать и проверять уже существующий код в SqlRepository. Для этого мы через Web.config находим базу (она должна располагаться локально), дублировать ее, подключаться к дубликату БД, проходить тесты и в конце, удалять дубликат БД.
    Создаем проект LessonProject.IntegrationTest в папке Test.
    Добавляем Ninject, Moq и NUnit:

    Install-Package Ninject Install-Package Moq Install-Package NUnit  

    Так же создаем папку Sandbox и в Setup наследуем UnitTestSetupFixture (/Setup/IntegrationTestSetupFixture.cs) и функцию по копированию БД:

    [SetUpFixture]     public class IntegrationTestSetupFixture : UnitTestSetupFixture     {         public class FileListRestore         {             public string LogicalName { get; set; }             public string Type { get; set; }         }          protected static string NameDb = "LessonProject";          protected static string TestDbName;          private void CopyDb(StandardKernel kernel, out FileInfo sandboxFile, out string connectionString)         {             var config = kernel.Get<IConfig>();             var db = new DataContext(config.ConnectionStrings("ConnectionString"));              TestDbName = string.Format("{0}_{1}", NameDb, DateTime.Now.ToString("yyyyMMdd_HHmmss"));              Console.WriteLine("Create DB = " + TestDbName);             sandboxFile = new FileInfo(string.Format("{0}\\{1}.bak", Sandbox, TestDbName));             var sandboxDir = new DirectoryInfo(Sandbox);              //backupFile             var textBackUp = string.Format(@"-- Backup the database             BACKUP DATABASE [{0}]             TO DISK = '{1}'             WITH COPY_ONLY",             NameDb, sandboxFile.FullName);             db.ExecuteCommand(textBackUp);              var restoreFileList = string.Format("RESTORE FILELISTONLY FROM DISK = '{0}'", sandboxFile.FullName);             var fileListRestores = db.ExecuteQuery<FileListRestore>(restoreFileList).ToList();             var logicalDbName = fileListRestores.FirstOrDefault(p => p.Type == "D");             var logicalLogDbName = fileListRestores.FirstOrDefault(p => p.Type == "L");              var restoreDb = string.Format("RESTORE DATABASE [{0}] FROM DISK = '{1}' WITH FILE = 1, MOVE N'{2}' TO N'{4}\\{0}.mdf', MOVE N'{3}' TO N'{4}\\{0}.ldf', NOUNLOAD, STATS = 10", TestDbName, sandboxFile.FullName, logicalDbName.LogicalName, logicalLogDbName.LogicalName, sandboxDir.FullName);             db.ExecuteCommand(restoreDb);              connectionString = config.ConnectionStrings("ConnectionString").Replace(NameDb, TestDbName);         }      }  

    По порядку:
    В строках

                var config = kernel.Get<IConfig>();             var db = new DataContext(config.ConnectionStrings("ConnectionString")); 

    — получаем подключение к БД.

    TestDbName = string.Format("{0}_{1}", NameDb, DateTime.Now.ToString("yyyyMMdd_HHmmss")); 

    Создаем наименование тестовой БД.

    //backupFile             var textBackUp = string.Format(@"-- Backup the database             BACKUP DATABASE [{0}]             TO DISK = '{1}'             WITH COPY_ONLY",             NameDb, sandboxFile.FullName);             db.ExecuteCommand(textBackUp); 

    — выполняем бекап БД в папку Sandbox.

                var restoreFileList = string.Format("RESTORE FILELISTONLY FROM DISK = '{0}'", sandboxFile.FullName);             var fileListRestores = db.ExecuteQuery<FileListRestore>(restoreFileList).ToList();             var logicalDbName = fileListRestores.FirstOrDefault(p => p.Type == "D");             var logicalLogDbName = fileListRestores.FirstOrDefault(p => p.Type == "L"); 

    — получаем логическое имя БД и файла логов, используя приведение к классу FIleListRestore.

                var restoreDb = string.Format("RESTORE DATABASE [{0}] FROM DISK = '{1}' WITH FILE = 1, MOVE N'{2}' TO N'{4}\\{0}.mdf', MOVE N'{3}' TO N'{4}\\{0}.ldf', NOUNLOAD, STATS = 10", TestDbName, sandboxFile.FullName, logicalDbName.LogicalName, logicalLogDbName.LogicalName, sandboxDir.FullName);             db.ExecuteCommand(restoreDb); 

    — восстанавливаем БД под другим именем (TestDbName)

     connectionString = config.ConnectionStrings("ConnectionString").Replace(NameDb, TestDbName); 

    — меняем connectionString.

    И теперь можем спокойно проинициализировать IRepository к SqlRepository:

    protected override void InitRepository(StandardKernel kernel)         {             FileInfo sandboxFile;             string connectionString;             CopyDb(kernel, out sandboxFile, out connectionString);             kernel.Bind<webTemplateDbDataContext>().ToMethod(c =>  new webTemplateDbDataContext(connectionString));             kernel.Bind<IRepository>().To<SqlRepository>().InTransientScope();             sandboxFile.Delete();         } 

    Итак, у нас есть sandboxFile – это файл бекапа, и connectionString – это новая строка подключения (к дубликату БД). Мы копируем БД, связываем именно с SqlRepository, но базу подсовываем не основную. И с ней можно делать всё что угодно. Файл бекапа базы в конце удаляем.
    И дописываем уже удаление тестовой БД, после прогона всех тестов:

    private void RemoveDb()         {             var config =  DependencyResolver.Current.GetService<IConfig>();              var db = new DataContext(config.ConnectionStrings("ConnectionString"));              var textCloseConnectionTestDb = string.Format(@"ALTER DATABASE [{0}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE", TestDbName);             db.ExecuteCommand(textCloseConnectionTestDb);              var textDropTestDb = string.Format(@"DROP DATABASE [{0}]", TestDbName);             db.ExecuteCommand(textDropTestDb);         } 

    Используя TestDbName, закрываем подключение (а то оно активное), и удаляем базу данных.
    Не забываем сделать копию Web.config:

    xcopy $(SolutionDir)LessonProject\Web.config $(ProjectDir)Sandbox\ /y 

    Но кстати, иногда БД нет необходимости удалять. Например, мы хотим заполнить базу кучей данных автоматически, чтобы проверить поиск или пейджинг. Это мы рассмотрим ниже. А сейчас тест – реальное создание в БД записи:

    [TestFixture]     public class DefaultUserControllerTest     {         [Test]         public void CreateUser_CreateNormalUser_CountPlusOne()         {             var repository = DependencyResolver.Current.GetService<IRepository>();              var controller = DependencyResolver.Current.GetService<LessonProject.Areas.Default.Controllers.UserController>();              var countBefore = repository.Users.Count();             var httpContext = new MockHttpContext().Object;              var route = new RouteData();              route.Values.Add("controller", "User");             route.Values.Add("action", "Register");             route.Values.Add("area", "Default");              ControllerContext context = new ControllerContext(new RequestContext(httpContext, route), controller);             controller.ControllerContext = context;              controller.Session.Add(CaptchaImage.CaptchaValueKey, "1111");              var registerUserView = new UserView()             {                 ID = 0,                 Email = "rollinx@gmail.com",                 Password = "123456",                 ConfirmPassword = "123456",                 Captcha = "1111",                 BirthdateDay = 13,                 BirthdateMonth = 9,                 BirthdateYear = 1970             };              Validator.ValidateObject<UserView>(registerUserView);             controller.Register(registerUserView);              var countAfter = repository.Users.Count();             Assert.AreEqual(countBefore + 1, countAfter);         }     } 

    Проверьте, что нет в БД пользователя с таким email.
    Запускаем, проверяем. Работает. Кайф! Тут понятно, какие мощности открываются. И если юнит-тестирование – это как обработка минимальных кусочков кода, а тут – это целый сценарий. Но, кстати, замечу, что MailNotify всё же высылает письма на почту. Так что перепишем его как сервис:
    /LessonProject/Tools/Mail/IMailSender.cs:

    public interface IMailSender     {         void SendMail(string email, string subject, string body, MailAddress mailAddress = null);     } 

    /LessonProject/Tools/Mail/MailSender.cs:

    public class MailSender : IMailSender     {         [Inject]         public IConfig Config { get; set; }          private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();          public void SendMail(string email, string subject, string body, MailAddress mailAddress = null)         {             try             {                 if (Config.EnableMail)                 {                     if (mailAddress == null)                     {                         mailAddress = new MailAddress(Config.MailSetting.SmtpReply, Config.MailSetting.SmtpUser);                     }                     MailMessage message = new MailMessage(                         mailAddress,                         new MailAddress(email))                     {                         Subject = subject,                         BodyEncoding = Encoding.UTF8,                         Body = body,                         IsBodyHtml = true,                         SubjectEncoding = Encoding.UTF8                     };                     SmtpClient client = new SmtpClient                     {                         Host = Config.MailSetting.SmtpServer,                         Port = Config.MailSetting.SmtpPort,                         UseDefaultCredentials = false,                         EnableSsl = Config.MailSetting.EnableSsl,                         Credentials =                             new NetworkCredential(Config.MailSetting.SmtpUserName,                                                   Config.MailSetting.SmtpPassword),                         DeliveryMethod = SmtpDeliveryMethod.Network                     };                     client.Send(message);                 }                 else                 {                     logger.Debug("Email : {0} {1} \t Subject: {2} {3} Body: {4}", email, Environment.NewLine, subject, Environment.NewLine, body);                 }             }             catch (Exception ex)             {                 logger.Error("Mail send exception", ex.Message);             }         }     } 


    /LessonProject/Tools/Mail/NotifyMail.cs:

    public static class NotifyMail     {         private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();          private static IConfig _config;          public static IConfig Config         {             get             {                 if (_config == null)                 {                     _config = (DependencyResolver.Current).GetService<IConfig>();                  }                 return _config;             }         }          private static IMailSender _mailSender;          public static IMailSender MailSender         {             get             {                 if (_mailSender == null)                 {                     _mailSender = (DependencyResolver.Current).GetService<IMailSender>();                  }                 return _mailSender;             }         }          public static void SendNotify(string templateName, string email,             Func<string, string> subject,             Func<string, string> body)         {             var template = Config.MailTemplates.FirstOrDefault(p => string.Compare(p.Name, templateName, true) == 0);             if (template == null)             {                 logger.Error("Can't find template (" + templateName + ")");             }             else             {                 MailSender.SendMail(email,                     subject.Invoke(template.Subject),                     body.Invoke(template.Template));             }         }     }  

    /LessonProject/App_Start/NinjectWebCommon.cs:

    private static void RegisterServices(IKernel kernel)         {… kernel.Bind<IMailSender>().To<MailSender>();         }         

    Ну и в LessonProject.UnitTest добавим MockMailSender (/Mock/Mail/MockMailSender.cs):

    public class MockMailSender : Mock<IMailSender>      {         public MockMailSender(MockBehavior mockBehavior = MockBehavior.Strict)             : base(mockBehavior)         {             this.Setup(p => p.SendMail(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MailAddress>()))                 .Callback((string email, string subject, string body, MailAddress address) =>                 Console.WriteLine(String.Format("Send mock email to: {0}, subject {1}", email, subject)));         }     } 

    В UnitTestSetupFixture.cs (/LessonProject.UnitTest/Setup/UnitTestSetupFixture.cs):

    protected virtual IKernel InitKernel()         { … kernel.Bind<MockMailSender>().To<MockMailSender>();             kernel.Bind<IMailSender>().ToMethod(p => kernel.Get<MockMailSender>().Object);             return kernel;         }  

    Запускаем, тесты пройдены, но на почту уже ничего не отправляется.

    =============== =====START===== =============== Create DB = LessonProject_20130314_104218 Send mock email to: chernikov@googlemail.com, subject Регистрация на   =============== =====BYE!====== =============== 
    Генерация данных

    Кроме всего прочего, мы можем и не удалять базу данных после пробегов теста. (переписать)Я добавлю GenerateData проект в папку Test, но подробно рассматривать мы его не будем, просто чтобы был. Он достаточно тривиальный. Суть его – есть некоторые наименования, и мы используем их для генерации. Например, для генерации фамилии используются фамилии американских президентов (зная их, мы сразу отличаем их от других фамилий, которые скорее будут реальными).

    Это также в будущем позволяет избежать «эффекта рыбы», когда в шаблоне тестовые данные были одной определенной, но не максимальной длины и шаблон выглядел прилично, но при использовании реальных данных всё поехало.
    Создадим 100 пользователей и потом посмотрим на них:

    [Test]         public void CreateUser_Create100Users_NoAssert()         {             var repository = DependencyResolver.Current.GetService<IRepository>();             var controller = DependencyResolver.Current.GetService<LessonProject.Areas.Default.Controllers.UserController>();              var httpContext = new MockHttpContext().Object;              var route = new RouteData();              route.Values.Add("controller", "User");             route.Values.Add("action", "Register");             route.Values.Add("area", "Default");              ControllerContext context = new ControllerContext(new RequestContext(httpContext, route), controller);             controller.ControllerContext = context;              controller.Session.Add(CaptchaImage.CaptchaValueKey, "1111");              var rand = new Random((int)DateTime.Now.Ticks);             for (int i = 0; i < 100; i++)             {                 var registerUserView = new UserView()                 {                     ID = 0,                     Email = Email.GetRandom(Name.GetRandom(), Surname.GetRandom()),                     Password = "123456",                     ConfirmPassword = "123456",                     Captcha = "1111",                     BirthdateDay = rand.Next(28) + 1,                     BirthdateMonth = rand.Next(12) + 1,                     BirthdateYear = 1970 + rand.Next(20)                 };                  controller.Register(registerUserView);             }         }  

    В IntegrationTestSetupFixture.cs отключим удаление БД после работы (/Setup/IntegrationTestSetupFixture.cs):

            protected static bool removeDbAfter = false; 

    В Web.config установим соединение с тестовой БД:

    <add name="ConnectionString" connectionString="Data Source=SATURN-PC;Initial Catalog=LessonProject_20130314_111020;Integrated Security=True;Pooling=False" providerName="System.Data.SqlClient" />  

    И запустим сайт:

    Итог

    В этом уроке мы рассмотрели:

    • Принципы TDD и когда они не срабатывают
    • NUnit и как с ним работать
    • Mock и как с ним работать
    • Unit-тесты и как этот инструмент позволяет улучшить нам качество кода
    • Integration-тесты, и как мы можем их использовать

    Тестирование – это очень большая область, это даже отдельная профессия и склад ума (не совсем программистский). И качество кода будет зависеть не только от применения технологий, хотя, бесспорно, соблюдение логических принципов TDD и внутренних процессов при разработке программ позволяет избежать множества ошибок. Написание тестов – не панацея от всех бед, это инструмент, и важно правильно им пользоваться…
    Мы обошли вниманием тестирование клиентской части, и честно говоря, я не знаю, как это должно происходить. В JQuery только в октябре 2011го начали развивать проект qUnit, но информации по нему почти нет.

    Все исходники находятся по адресу https://bitbucket.org/chernikov/lessons

ссылка на оригинал статьи http://habrahabr.ru/post/176137/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *