使用Mock、Repository和UnitOfWork进行BLL测试的C#单元测试

4
我在我的项目中实现了 Repository 和 Unit Of Work,并且使用了相同的架构,这里是参考链接 https://github.com/ahmadnegm/Publishers.EF.CodeFirst.Repository.UnitOfWork.Demo。所以,在我的项目中有三层(DAL、BLL和UI),我打算在 BLL 的单元测试中使用 Mocking,但是对于这个架构如何使用它感到非常困惑,因为我有一些模型在 BLL 中使用,这就是我需要测试的内容。
注:我阅读了一些话题,比如这个,这个和这个,但实际上我没有得到符合我的情况,所以,如果您可以指导我如何在我的单元测试中使用 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编写了一个扩展方法,可以将实体转换为模型,反之亦然。对于每个模型,我都有一个类,其中包含我需要实现该实体的所有逻辑。以下是我需要使用Mock测试的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个回答

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)和工厂模式的资料,例如这里
现在,在您的单元测试中,您可以简单地使用自己的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!

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


非常感谢,我会尝试一下,并且会再联系您。 - Ahmed Negm

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接