Добрый день!
Я пока что обнаружил рабочий вариант, который очень хорошо тестируется.
1. Создаем несколько проектов:
- DAL.Abstract - лежат интерфейсы Generic-репозитория, специализированных репозиториев и UnitOfWork'а;
- DAL.Real - содержит реализации интерфейсов из DAL.Abstract;
- DAL.Fakes - fake-реализации для юнит-тестов;
- BL.Abstract - лежат интерфейсы бизнес-сервисов;
- BL.Real - здесь реализации интерфейсов бизнес-сервисов;
- BL.Fakes - fake-реализации вспомогательных сервисов (EmailService, SmsService из Identity);
- BL.Tests - юнит-тесты бизнес-логики;
- Core.Models - модели предметной области (code-first).
2. Т.к. я использую шаблон UnitOfWork, то пришлось отказаться от готовой реализации из библиотеки Identity.EntityFramework в силу того, что в ней расположены уже готовые классы User, Role, Claims и др., и приходится эту библиотеку тащить
во все проекты, где нужно использовать хотя бы User'а. А вслед за ней во все проекты тащится библиотека EntityFramework... Мне это не нравится, потому что, на мой взгляд, слой MVC
не должен знать про частности реализации (т.е. про EntityFramework).
Поэтому самый простой вариант - сделать велоси... гм, собственную реализацию всяких там IUserStore, IPasswordStore и др.интерфейсов из Microsoft.AspNet.Identity.Core (ну и соответственно, создать собственные классы User, Role, UserRole, Claims, ExternalLogin), которые могли бы работать с нужным мне UnitOfWork'ом.
3. Пишем тест:
[TestFixture]
public class UserServiceTests
{
private FakeEmailService _emaiService;
private FakeSmsService _smsService;
private IUserServiceFactory _userServiceFactory;
private IUserService _userSrv;
private IAuthenticationManager _authManager;
private IUserRepository _userRepository;
[SetUp]
public void PreInit()
{
var authManagerMock = new Mock<IAuthenticationManager>();
_authManager = authManagerMock.Object;
_userRepository = new FakeUserRepository();
_smsService = new FakeSmsService();
_emaiService = new FakeEmailService();
_userServiceFactory = new UserServiceFactory(
new FakeUnitOfWorkFactory(_userRepository),
_emaiService, _smsService, _authManager);
_userSrv = _userServiceFactory.Create();
// Заполняем хранилище пользователем "exist@exist.ru"
RegisterExistUser();
}
private RegisterResult RegisterExistUser()
{
var model = new RegisterNewUserViewModel(ConstForTest.Users.ExistUser.Email, ConstForTest.Users.ExistUser.Password);
return RegisterUser(model);
}
// Регистрация пользователя "valid@user.ru"
private RegisterResult RegisterValidUser()
{
var model = new RegisterNewUserViewModel(ConstForTest.Users.ValidUser.Email, ConstForTest.Users.ValidUser.Password);
return RegisterUser(model);
}
[Category(ConstForTest.Categories.UserServiceTests.RegisterNewUser)]
[Test]
public void RegisterNewUser_ValidEmailAndPassword_AfterRegistrationEmailDoesNotConfirmed()
{
var email = ConstForTest.Users.ValidUser.Email;
// Act
RegisterValidUser();
var foundUser = _userSrv.GetUser(email);
Assert.IsNotNull(foundUser);
Assert.IsFalse(foundUser.EmailConfirmed);
}
}
4. Пишем логику бизнес-сервисов. Приведу пример регистрации юзера:
public class UserService : IUserService
{
private readonly IUnitOfWorkFactory _unitFactory;
private readonly IUserIdentityManagerFactory _userManagerFactory;
private readonly ISignInManagerFactory _signInManagerFactory;
public UserService(IUnitOfWorkFactory unitFactory,
IUserIdentityManagerFactory userManagerFactory,
ISignInManagerFactory signInManagerFactory)
{
_unitFactory = unitFactory;
_userManagerFactory = userManagerFactory;
_signInManagerFactory = signInManagerFactory;
}
public RegisterResult RegisterNewUser(RegisterNewUserViewModel model)
{
using (var unit = _unitFactory.Create())
{
RegisterResult result = new RegisterResult();
IdentityResult identityResult;
try
{
var username = model.Email;
var manager = CreateUserManager(unit.UserRepository);
identityResult = manager.Create(new User(username), model.Password);
if (identityResult.Succeeded)
{
var createdUser = unit.UserRepository.GetByUsernameQuery(username);
manager.AddToRole(createdUser.Id, Roles.Client.ToString());
var confirmEmailCode = manager.GenerateEmailConfirmationToken(createdUser.Id);
result.ConfirmEmailCode = confirmEmailCode;
manager.SendEmail(createdUser.Id, Const.EmailSubjects.Account.ConfirmEmail,
"<Тут содержимое письма для подтверждения е-майл пользователя>");
}
unit.Commit();
}
catch (Exception ex)
{
identityResult = IdentityResult.Failed("В процессе регистрации возникла ошибка. Попробуйте выполнить операцию еще раз.");
unit.Rollback();
}
result.IdentityResult = identityResult;
return result;
}
}
// ... другие методы бизнес-сервиса
}
Метод CreateUserManager(unit.UserRepository) создает экземпляр класса UserManager из библиотеки Microsoft.AspNet.Identity), передавая ему репозиторий работы с пользователями (да-да, т.к. у нас Identity, то необходимо вынести репозиторий для юзеров в отдельный интерфейс IUserRepository):
private UserManager<User, Guid> CreateUserManager(IUserRepository userRepository)
{
return _userManagerFactory.Create(userRepository);
}
Фабрика создания UnitOfWork'а выглядит так:
public interface IUnitOfWorkFactory
{
IUnitOfWork Create();
IUnitOfWork Create(IsolationLevel level);
}
public class EfUnitOfWorkFactory : IUnitOfWorkFactory
{
public IUnitOfWork Create()
{
return Create(IsolationLevel.ReadCommitted);
}
public IUnitOfWork Create(IsolationLevel level)
{
var uow = new EfUnitOfWork(level);
return uow;
}
}
Сам UnitOfWork:
public interface IUnitOfWork : IDisposable, IUnitOfWorkRepositories
{
void SaveChanges();
void Commit();
void Rollback();
}
public interface IUnitOfWorkRepositories
{
IUserRepository UserRepository { get; }
}
public class EfUnitOfWork : IUnitOfWork
{
private bool _isDisposed = false;
private DbContext _db;
private DbContextTransaction _transaction;
public EfUnitOfWork(IsolationLevel level = IsolationLevel.ReadCommitted)
{
_db = new MyApplicationDbContext();
_transaction = _db.Database.BeginTransaction(level);
}
#region IDisposable
protected virtual void Dispose(bool disposing)
{
if (!_isDisposed)
{
if (disposing)
{
if (_transaction != null)
{
_transaction.Rollback();
_transaction.Dispose();
_transaction = null;
}
_db.Dispose();
_db = null;
}
}
_isDisposed = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
#region IUnitOfWork
public void SaveChanges()
{
_db.SaveChanges();
}
public void Commit()
{
SaveChanges();
_transaction.Commit();
}
public void Rollback()
{
_transaction.Rollback();
}
#endregion
private IUserRepository _userRepo;
public IUserRepository UserRepository
{
get { return _userRepo ?? (_userRepo = new EfUserRepository(_db)); }
}
}
В итоге мы получаем, на мой взгляд, хорошо тестируемую архитектуру. При этом можно написать десятки тестов, проверяющих функционал бизнес-сервиса работы с пользователями, в изоляции от MVC, EntityFramework и др. конкретных реализаций.
P.S.
1. Буду рад замечаниям со стороны опытных коллег относительно данной реализации.
2. В качестве основы взяты материалы
цикла статей
3. Я придерживаюсь той концепции, что ни один Repository
не должен знать про SaveChanges, а уж тем более про Commit и Rollback. Это прерогатива UnitOfWork'а, а Repository должен реализовывать лишь CRUD-операции. Поэтому метод SaveChanges вынесен в интерфейс IUnitOfWork.