使用Mock,Repository和UnitOfWork C#UnitTesting测试BLL

时间:2017-08-13 09:08:18

标签: c# entity-framework unit-testing repository unit-of-work

我在项目中实现了Repository和Unit Of Work,我正在使用相同的架构here。所以,我的项目中有3层( DAL BLL UI ),我将使用 Mocking 在BLL的单元测试中,但是我对这个架构使用它感到困惑,因为我有BLL中使用的模型,这是我需要测试的。

注意:我已经阅读了一些主题,例如thisthisthis,但实际上我没有得到与我匹配的案例,所以如果你可以指导我完成这项工作会很棒,如何在我的单元测试中使用Mocking。

代码示例:DAL

IUnitOfWork

public interface IUnitOfWork : IDisposable
{
    IRepository<T> GetRepository<T>() where T : Entity;
    int Save();
    int SaveInDbTransaction(IsolationLevel isolationLevel = IsolationLevel.ReadCommitted);
    string ErrorMessage { get; }
}

IRepository

public interface IRepository<TEntity> : IDisposable
{
    IQueryable<TEntity> All(Expression<Func<TEntity, bool>> filter = null, Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null, string includeProperties = "");

    IEnumerable<TEntity> GetWithRawSql(string query, params object[] parameters);

    IQueryable<TEntity> Filter(Expression<Func<TEntity, bool>> predicate);

    IQueryable<TEntity> Filter<TKey>(Expression<Func<TEntity, bool>> filter, out int total, int index = 0, int size = 50);

    bool Contains(Expression<Func<TEntity, bool>> predicate);

    TEntity Find(params object[] keys);

    TEntity Find(Expression<Func<TEntity, bool>> predicate);

    void Create(TEntity entity);

    void Delete(object entityId);

    void Delete(TEntity entity);

    void Delete(Expression<Func<TEntity, bool>> predicate);

    void Update(TEntity entity);

    int Count { get; }
}

的UnitOfWork

public class UnitOfWork<TContext> : IUnitOfWork where TContext : DbContext
{
    public string ErrorMessage { get; private set; }
    private readonly TContext _context;
    private bool _disposed;
    private Dictionary<string, object> _repositories;

    public UnitOfWork()
    {
        ErrorMessage = null;
        _context = Activator.CreateInstance<TContext>();
        _repositories = new Dictionary<string, object>();
    }

    public UnitOfWork(TContext context)
    {
        _context = context;
        ErrorMessage = null;
    }

    public IRepository<TSet> GetRepository<TSet>() where TSet : Entity
    {
        if (_repositories == null)
        {
            _repositories = new Dictionary<string, object>();
        }

        if (_repositories.ContainsKey(typeof(TSet).Name))
        {
            return _repositories[typeof(TSet).Name] as IRepository<TSet>;
        }

        var repositoryInstance = new Repository<TSet, TContext>(_context);
        _repositories.Add(typeof(TSet).Name, repositoryInstance);
        return repositoryInstance;
    }

    public int Save()
    {
        try
        {
            #region Handling auditing

            var modifiedEntries = _context.ChangeTracker.Entries()
                .Where(x => x.Entity is IAuditableEntity
                            && (x.State == EntityState.Added ||
                                x.State == EntityState.Modified));

            foreach (var entry in modifiedEntries)
            {
                var entity = entry.Entity as IAuditableEntity;
                if (entity != null)
                {
                    var identityName = Thread.CurrentPrincipal.Identity.Name;
                    var now = DateTime.UtcNow;

                    if (entry.State == EntityState.Added)
                    {
                        entity.CreatedBy = identityName;
                        entity.Created = now;
                    }
                    else
                    {
                        _context.Entry(entity).Property(x => x.CreatedBy).IsModified = false;
                        _context.Entry(entity).Property(x => x.Created).IsModified = false;
                    }

                    entity.ModifiedBy = identityName;
                    entity.Modified = now;
                }
            } 
            #endregion

            var affectedRows = _context.SaveChanges();
            return affectedRows;
        }
        catch (DbEntityValidationException dbEx)
        {
            foreach (var validationError in dbEx.EntityValidationErrors.SelectMany(
                validationErrors => validationErrors.ValidationErrors))
            {
                ErrorMessage += $"Property: {validationError.PropertyName} Error: {validationError.ErrorMessage}" +
                                Environment.NewLine;
            }
            throw new Exception(ErrorMessage, dbEx);
        }
        catch (Exception exception)
        {
            ErrorMessage = exception.Message;
            throw new Exception(ErrorMessage, exception);
        }
    }

    public int SaveInDbTransaction(IsolationLevel isolationLevel = IsolationLevel.ReadCommitted)
    {
        DbContextTransaction transaction = null; 
        try
        {
            transaction = _context.Database.BeginTransaction(IsolationLevel.ReadCommitted);
            using (transaction)
            {
                #region Handling auditing

                var modifiedEntries = _context.ChangeTracker.Entries()
                    .Where(x => x.Entity is IAuditableEntity
                                && (x.State == EntityState.Added ||
                                    x.State == EntityState.Modified));

                foreach (var entry in modifiedEntries)
                {
                    var entity = entry.Entity as IAuditableEntity;
                    if (entity != null)
                    {
                        var identityName = Thread.CurrentPrincipal.Identity.Name;
                        var now = DateTime.UtcNow;

                        if (entry.State == EntityState.Added)
                        {
                            entity.CreatedBy = identityName;
                            entity.Created = now;
                        }
                        else
                        {
                            _context.Entry(entity).Property(x => x.CreatedBy).IsModified = false;
                            _context.Entry(entity).Property(x => x.Created).IsModified = false;
                        }

                        entity.ModifiedBy = identityName;
                        entity.Modified = now;
                    }
                }

                #endregion

                var affectedRows = _context.SaveChanges();
                transaction.Commit();
                return affectedRows;
            }
        }
        catch (DbEntityValidationException dbEx)
        {
            foreach (var validationError in dbEx.EntityValidationErrors.SelectMany(
                validationErrors => validationErrors.ValidationErrors))
            {
                ErrorMessage += $"Property: {validationError.PropertyName} Error: {validationError.ErrorMessage}" +
                                Environment.NewLine;
            }
            transaction?.Rollback();
            throw new Exception(ErrorMessage, dbEx);
        }
        catch (Exception exception)
        {
            ErrorMessage = exception.Message;
            transaction?.Rollback();
            throw new Exception(ErrorMessage, exception);
        }
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    public virtual void Dispose(bool disposing)
    {
        if (!_disposed)
        {
            if (disposing)
            {
                _context.Dispose();
            }
        }
        _disposed = true;
    }
}

存储库

public class Repository<TEntity, TContext> : IRepository<TEntity>
    where TEntity : Entity
    where TContext : DbContext
{
    private readonly TContext _context;
    protected DbSet<TEntity> DbSet => _context.Set<TEntity>();

    public Repository(TContext session)
    {
        _context = session;
    }

    public void Dispose()
    {
        _context?.Dispose();
    }

    public IQueryable<TEntity> All(Expression<Func<TEntity, bool>> filter = null, Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null, string includeProperties = "")
    {
        //return DbSet.AsQueryable();
        var query = DbSet.AsQueryable();

        if (filter != null)
        {
            query = query.Where(filter);
        }

        query = includeProperties.Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries)
            .Aggregate(query, (current, includeProperty) => current.Include(includeProperty));

        return orderBy?.Invoke(query).AsQueryable() ?? query.AsQueryable();
    }

    public IEnumerable<TEntity> GetWithRawSql(string query, params object[] parameters)
    {
        return DbSet.SqlQuery(query, parameters).ToList();
    }

    public IQueryable<TEntity> Filter(Expression<Func<TEntity, bool>> predicate)
    {
        return DbSet.Where(predicate).AsQueryable();
    }

    public IQueryable<TEntity> Filter<TKey>(Expression<Func<TEntity, bool>> predicate, out int total, int index = 0,
        int size = 50)
    {
        var result = DbSet.Where(predicate);
        total = result.Count();
        return result.Skip(index).Take(size);
    }

    public bool Contains(Expression<Func<TEntity, bool>> predicate)
    {
        return DbSet.Count(predicate) > 0;
    }

    public TEntity Find(params object[] keys)
    {
        return DbSet.Find(keys);
    }

    public TEntity Find(Expression<Func<TEntity, bool>> predicate)
    {
        return DbSet.FirstOrDefault(predicate);
    }

    public void Create(TEntity entity)
    {
        DbSet.Add(entity);
    }

    public void Delete(object entityId)
    {
        var entity = DbSet.Find(entityId);
        if (entity != null)
        {
            DbSet.Remove(entity);
        }
    }

    public void Delete(TEntity entity)
    {
        DbSet.Remove(entity);
    }

    public void Delete(Expression<Func<TEntity, bool>> predicate)
    {
        var objects = Filter(predicate);
        foreach (var obj in objects)
            DbSet.Remove(obj);
    }

    public void Update(TEntity entity)
    {
        var entry = _context.Entry(entity);
        DbSet.Attach(entity);
        entry.State = EntityState.Modified;
    }

    public int Count => DbSet.Count();

}

BLL:

在BLL中,我为DAL中的每个实体建立了一个与UI层进行通信的模型,并且有一个扩展方法,使用 AutoMapper 从实体转换为模型,反之亦然,我有一个类对于每个模型,其中包含我需要使用该实体实现的所有逻辑,这里是一个BLL类的示例,我需要使用模拟来测试它:

public class ClientManager
{
    public int Add(ClientModel model)
    {
        var entity = model.ToEntity();
        using (var uow = new UnitOfWork<SubscriptionContext>())
        {
            if (model.IsValid())
            {
                var entityRepository = uow.GetRepository<Data.Entities.Client>();
                entityRepository.Create(entity);
                var affected = uow.Save();
                if (affected < 1)
                {
                    throw new Exception(uow.ErrorMessage);
                }

                Logger.Log(Logger.LogLevel.Information, this.GetType().FullName, MethodBase.GetCurrentMethod(), "Adding new entity: " + entity.Id, null, Thread.CurrentPrincipal.Identity.Name);
                return affected;
            }
            else
            {
                throw new Exception("Model is not valid.");
            }
        }
    }

    public int Update(ClientModel model)
    {
        var entity = model.ToEntity();

        using (var uow = new UnitOfWork<SubscriptionContext>())
        {
            if (model.IsValid())
            {
                var entityRepository = uow.GetRepository<Data.Entities.Client>();
                entityRepository.Update(entity);
                var affected = uow.Save();
                if (affected < 1)
                {
                    throw new Exception(uow.ErrorMessage);
                }
                Logger.Log(Logger.LogLevel.Information, this.GetType().FullName, MethodBase.GetCurrentMethod(), "Updating existing entity: " + entity.Id, null, Thread.CurrentPrincipal.Identity.Name);
                return affected;

            }
            else
            {
                throw new Exception("Model is not valid.");
            }
        }
    }

    public int Delete(int entityId)
    {
        using (var uow = new UnitOfWork<SubscriptionContext>())
        {
            if (entityId > 0)
            {
                var entityRepository = uow.GetRepository<Data.Entities.Client>();
                entityRepository.Delete(entityId);
                var affected = uow.Save();
                if (affected < 1)
                {
                    throw new Exception(uow.ErrorMessage);
                }
                Logger.Log(Logger.LogLevel.Information, this.GetType().FullName, MethodBase.GetCurrentMethod(), "Removing existing entity: " + entityId, null, Thread.CurrentPrincipal.Identity.Name);
                return affected;
            }
            else
            {
                throw new Exception("There is no data to delete at the current position.");
            }
        }
    }

    public ClientModel Find(int entityId)
    {
        using (var uow = new UnitOfWork<SubscriptionContext>())
        {
            if (entityId > 0)
            {
                var entityRepository = uow.GetRepository<Data.Entities.Client>();
                var entity = entityRepository.Find(entityId);
                if(entity != null) { 
                return entity.ToModel();

                }
            }
            throw new Exception("There is no data to delete at the current position.");
        }
    }
}

1 个答案:

答案 0 :(得分:2)

您想要模拟,但您似乎没有使用任何依赖注入。相反,您只需在任何需要的地方创建自己的UnitOfWork<SubscriptionContext>实现。

我建议您查看依赖注入,并实际注册UnitOfWorkFactory以插入ClientManager

您的代码看起来像这样:

public class ClientManager
{
    private readonly IUnitOfWorkFactory UowFactory;

    public ClientManager(IUnitOfWorkFactory<SubscriptionContext> uowFactory)
    {
        UowFactory = uowFactory;
    }

    public int Add(ClientModel model)
    {
        var entity = model.ToEntity();
        using (var uow = uowFactory.GetUoW())
        {  
            // dowork
        }
    }
}

您可以阅读依赖注入(例如使用unity)和工厂模式onlin,例如here

现在,在您的单元测试中,您可以简单地使用自己的IUnitOfWorkFactory实现,在该实现中返回模拟UoW,如下所示:

var UowMock = new Mock<IUnitOfWork<SubscriptionContext>();
var UowFactoryMock = new Mock<IUowFactory>();
UowFactoryMock.Stub(f => f.GetUoW()).Returns(UowMock);

var clientManager = new ClientManager(UowFactoryMock);
// Test whatever you want in your clientManager!

当然,您可能必须设置工作单元以在调用方法时返回预期值。如何做到这完全取决于您的测试框架。