Как известно использование сильно связанных классов это плохо, так как попытка внести изменения в один класс, неизбежно повлекут за собой, целую цепочку изменений в других классах. Особенно это проявляется при поддержке больших систем, состоящих из огромного числа классов. Кроме того, такой подход увеличивает сложность тестирования, так как один класс невозможно протестировать отдельно от другого. Для уменьшения связанности классов необходимо программировать на уровне интерфейсов, а не на уровне реализаций, т.е. всю логику, которая гипотетически может измениться необходимо, по возможности, выносить в интерфейс. Затем, при создании класса, в его конструктор можно передать объект, реализующий необходимый интерфейс и им инициализировать объект внутри класса (пример). Использование такого подхода описывает паттерн Dependency Injection. Многие проблемы, возникающие при развитии системы, могут быть решены, если при программировании логики работы стараться придерживаться принципов проектирования классов (S.O.L.I.D). При переходе к программированию на уровне интерфейсов создание объектов может сильно усложниться, так как каждый раз при создании нового объекта нам необходимо будет настраивать его зависимости. Для простых объектов это может показаться не очень сложным, однако при усложнении системы простая инициализация объекта будет вести за собой целую цепочку вызовов разрешений зависимостей, и это будет необходимо делать при каждой инициализации объекта. Для решения этой проблемы существует множество DI frameworks, которые при правильной настройке позволяют легко инициализировать объекты вместе с их зависимостями. В своей работе я использовал два framework-а: Unity и Ninject. Скажу честно, какой-то принципиальной разницы между ними не нашел, кроме того, что Ninject интегрируется с MVC «из коробки» и еще мелких различий, поэтому тут опишу только Ninject.
Для начала Ninject необходимо добавить в проект через NuGet.
После этого в папке App_Start автоматически создастся класс NinjectWebCommon. В этом классе и происходит основная настройка Ninject. Но перед его настройкой, немного изменим класс UnitOfWork http://aykspace.blogspot.ru из предыдущего примера. Теперь он будет реализовывать интерфейс IUnitOfWork.
public interface IUnitOfWork { IRepository<User> UserRepository { get; } }
Теперь можно настраивать. В классе NinjectWebCommon найдем метод static void RegisterServices(IKernel kernel) и добавим привязку нашего IUnitOfWork к UnitOfWork и IRepository<User> к GenericRepository<User> :
Теперь в BaseController-e можно просто пометить атрибутом [Inject] свойство:
private static void RegisterServices(IKernel kernel) { kernel.Bind<IRepository<User>>().To<GenericRepository<User>>().InRequestScope(); kernel.Bind<IUnitOfWork>().To<UnitOfWork>().InRequestScope(); }InRequestScope – определяет границы существования объекта в рамках одного запроса.
Теперь в BaseController-e можно просто пометить атрибутом [Inject] свойство:
[Inject] public IUnitOfWork UnitOfWork { get; set; }и оно будет корректно проинициализировано согласно приведенным выше настройкам.
Тестируем все это.
Тесты будем проводить с помощью NUnit framework. Для начала его следует установить (http://sourceforge.net/projects/nunit). Затем, для того чтобы появилась возможность запускать тесты непосредственно через Visual Studio необходимо установить NUnit Test Adapter (через NuGet). После этого создадим в нашем решении новый проект типа Class Library и добавим к нему ссылку на тестируемый проект.
Начнем с простого, создадим UserController и проверим тип модели одного из него Actions.
Все тесты делятся на 3 части:
- Init – инициализация тестируемого объекта;
- Act – действие тестируемого объекта;
- Assert – проверка того или иного условия.
Напишем тест по этому образцу.
[TestFixture] public class UserControllerTest { [Test] public void Register_GetView_ItsOkViewModelIsUserView() { var controller = new UserController(); // init var result = controller.Register();//act Assert.IsInstanceOf<ViewResult>(result);//assert Assert.IsInstanceOf<UserView>(((ViewResult)result).Model); } }
Тестируем – работает!
Теперь протестируем взаимодействие с базой данных. Проверим, что при загрузке страницы Index контроллера User происходит чтение пользователей из БД.
Если написать что-то типа этого:
[Test] public void Index_GetUsers_CountOfUsersIsTwo() { var controller = new UserController(); var result = controller.Index(); Assert.IsInstanceOf<ViewResult>(result); Assert.IsInstanceOf<List<User>>(((ViewResult)result).Model); var count = ((List<User>)((ViewResult)result).Model).Count; Assert.That(count, Is.EqualTo(2)); }
то тест рухнет, даже не добравшись до Assert-a. Вся проблема этого кода, кроется только в одной строчке – с оператором new. В ней инициализируется наш UserController, но наш тестовый проект ничего не знает, ни про базу данных, ни про настроенный Ninject, поэтому при попытке вывести список пользователей методом Index() возникает ошибка. Если про dependency injection вроде бы все ясно (нужно просто добавить настройку зависимостей), то, что делать с базой данных (Unit of Work)? Тут нам могут помочь Mock – объекты.
Mock – объекты нужны для замены реального объекта в условиях теста, они позволяют проверять вызовы своих членов как часть системы или unit-теста.
Для создания mock – объектов воспользуемся фреймворком Moq. Добавим его в наш тестовый проект через NuGet. Затем создадим mock-объект для нашего интерфейса IUnitOfWork:
Mock – объекты нужны для замены реального объекта в условиях теста, они позволяют проверять вызовы своих членов как часть системы или unit-теста.
Для создания mock – объектов воспользуемся фреймворком Moq. Добавим его в наш тестовый проект через NuGet. Затем создадим mock-объект для нашего интерфейса IUnitOfWork:
public partial class MockUnitOfWork : Mock<IUnitOfWork> { public MockUnitOfWork(MockBehavior mockBehavior = MockBehavior.Strict) : base(mockBehavior) { GenerateUsers(); } public List<User> Users { get; set; } public GenericRepository<User> UserRepository { get; set; } public void GenerateUsers() { UserRepository = new GenericRepository<User>(); Users = new List<User>(); var admin = new User() { UserId = 1, ActivatedDate = null, ActivatedLink = Guid.NewGuid().ToString().Substring(1,4), AddedDate = DateTime.Now, AvatarPath = "", Birthdate = new DateTime(1988, 06, 07), Email = "a.y.kutomanov", Password = "1123" }; Users.Add(admin); var userOne = new User() { UserId = 2, ActivatedDate = null, ActivatedLink = Guid.NewGuid().ToString().Substring(1, 4), AddedDate = DateTime.Now, AvatarPath = "", Birthdate = new DateTime(1989, 06, 07), Email = "megaswed@mail.ru", Password = "11111" }; Users.Add(userOne); Setup(p => p.UserRepository.Get()).Returns(Users.AsEnumerable()); Setup(p => p.UserRepository).Returns(new GenericRepository<User>()); Setup(p => p.UserRepository.GetById(It.IsAny<int>())).Returns((string email) => Users.FirstOrDefault(p => string.Compare(p.Email, email, 0) == 0)); } }
Ключевым здесь является метод Setup, который как бы говорит, что необходимо делать mock-объекту в случае, когда у него что-то запрашивают, т.е. когда мы запросим метод Get() у свойства UserRepository объекта UnitOfWork, то вместо стандартного его поведения, включающего запрос к БД, он вернет нам наш инициализированный список пользователей.
Теперь перейдем к dependency injection, получать объект мы будем через стандартный класс DependencyResolve, который в качестве ресолвера будет принимать наш, настроенный Ninject ресолвер:
Теперь перейдем к dependency injection, получать объект мы будем через стандартный класс DependencyResolve, который в качестве ресолвера будет принимать наш, настроенный Ninject ресолвер:
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>(); } } }
Так как разрешать зависимости необходимо перед стартом наших тестов, создадим класс с атрибутом [SetUpFixture]. Этот атрибут показывает, что методы класса будут вызваны до проведения тестов. В методе SetUp() установим наш Resolver классу DependencyResolver и свяжем IUnitOfWork с нашим mock-объектом.
Еще про mock объекты можно прочитать тут.
[SetUpFixture] public class UnitTestSetupFixture { [SetUp] public virtual void Setup() { InitKernel(); } protected virtual IKernel InitKernel() { var kernel = new StandardKernel(); DependencyResolver.SetResolver(new NinjectDependencyResolver(kernel)); kernel.Bind<IUnitOfWork>().ToMethod(p => kernel.Get<MockUnitOfWork>().Object); return kernel; } }Теперь можно поменять первую строчку в нашем тестовом методе и тест пройдет:
[Test] public void Index_GetUsers_CountOfUsersIsTwo() { var controller = DependencyResolver.Current.GetService<UserController>(); var result = controller.Index(); Assert.IsInstanceOf<ViewResult>(result); Assert.IsInstanceOf<List<User>>(((ViewResult)result).Model); var count = ((List<User>)((ViewResult)result).Model).Count; Assert.That(count, Is.EqualTo(2)); }Важное замечание, если в UserController есть еще методы с атрибутом [Inject] необходимо, либо прописать зависимости в Setup(), либо убрать этот атрибут во время тестирования, иначе DependencyResolver вернет null.
Еще про mock объекты можно прочитать тут.
Комментариев нет:
Отправить комментарий