html текст
All interests
  • All interests
  • Design
  • Food
  • Gadgets
  • Humor
  • News
  • Photo
  • Travel
  • Video
Click to see the next recommended page
Like it
Don't like
Add to Favorites

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

Цель урока. Научиться создавать тесты для кода. 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
Читать дальше
Twitter
Одноклассники
Мой Мир

материал с habrahabr.ru

3

      Add

      You can create thematic collections and keep, for instance, all recipes in one place so you will never lose them.

      No images found
      Previous Next 0 / 0
      500
      • Advertisement
      • Animals
      • Architecture
      • Art
      • Auto
      • Aviation
      • Books
      • Cartoons
      • Celebrities
      • Children
      • Culture
      • Design
      • Economics
      • Education
      • Entertainment
      • Fashion
      • Fitness
      • Food
      • Gadgets
      • Games
      • Health
      • History
      • Hobby
      • Humor
      • Interior
      • Moto
      • Movies
      • Music
      • Nature
      • News
      • Photo
      • Pictures
      • Politics
      • Psychology
      • Science
      • Society
      • Sport
      • Technology
      • Travel
      • Video
      • Weapons
      • Web
      • Work
        Submit
        Valid formats are JPG, PNG, GIF.
        Not more than 5 Мb, please.
        30
        surfingbird.ru/site/
        RSS format guidelines
        500
        • Advertisement
        • Animals
        • Architecture
        • Art
        • Auto
        • Aviation
        • Books
        • Cartoons
        • Celebrities
        • Children
        • Culture
        • Design
        • Economics
        • Education
        • Entertainment
        • Fashion
        • Fitness
        • Food
        • Gadgets
        • Games
        • Health
        • History
        • Hobby
        • Humor
        • Interior
        • Moto
        • Movies
        • Music
        • Nature
        • News
        • Photo
        • Pictures
        • Politics
        • Psychology
        • Science
        • Society
        • Sport
        • Technology
        • Travel
        • Video
        • Weapons
        • Web
        • Work

          Submit

          Thank you! Wait for moderation.

          Тебе это не нравится?

          You can block the domain, tag, user or channel, and we'll stop recommend it to you. You can always unblock them in your settings.

          • habrahabr.ru
          • домен habrahabr.ru

          Get a link

          Спасибо, твоя жалоба принята.

          Log on to Surfingbird

          Recover
          Sign up

          or

          Welcome to Surfingbird.com!

          You'll find thousands of interesting pages, photos, and videos inside.
          Join!

          • Personal
            recommendations

          • Stash
            interesting and useful stuff

          • Anywhere,
            anytime

          Do we already know you? Login or restore the password.

          Close

          Add to collection

             

            Facebook

            Ваш профиль на рассмотрении, обновите страницу через несколько секунд

            Facebook

            К сожалению, вы не попадаете под условия акции