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

в 18:53, , рубрики: .net, ASP, asp.net mvc, nunit, tdd, метки: , , , ,

Цель урока. Научиться создавать тесты для кода. 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).

ASP.NET MVC Урок E. Тестирование
Создадим папочку типа 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:
ASP.NET MVC Урок E. Тестирование

Если адаптер 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:
    ASP.NET MVC Урок E. Тестирование

    xcopy $(SolutionDir)LessonProjectWeb.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)LessonProjectWeb.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" />
    
    

    И запустим сайт:
    ASP.NET MVC Урок E. Тестирование

    Итог

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

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

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

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

Автор: chernikov

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js