使用 UnitOfWork 模拟上下文和存储库

Posted

技术标签:

【中文标题】使用 UnitOfWork 模拟上下文和存储库【英文标题】:Mocking Context and Repository with UnitOfWork 【发布时间】:2017-03-28 15:49:01 【问题描述】:

我正在为我们需要构建的一个小应用程序构建单元测试。

我已经实现了存储库/工作单元模式。我的经理类实现了工作单元模式。

对于给定的接口:

public interface IUserManager

    List<ApplicationUser> GetUsers(Expression<Func<ApplicationUser, bool>> filter = null);
    ApplicationUser GetUser(Expression<Func<ApplicationUser, bool>> filter);
    ApplicationUser AddUser(string username, List<string> environmentIds, bool isAdmin = false);
    void DeleteUser(string username);
    ApplicationUser UpdateUser(string id, List<string> environmentIds, bool isAdmin = false);
    IList<string> GetUserRoles(string id);

我已经实现了

public class UserManager : IUserManager


    #region private fields

    private readonly IRepository<ApplicationUser> _userRepository;
    private readonly IRepository<Application> _applicationRepository;
    private readonly IRepository<Role> _roleRepository;
    private readonly IActiveDirectoryManager _activeDirectoryManager;


    #endregion

    #region ctor

    public UserManager(AppDbContext context, IActiveDirectoryManager activeDirectoryManager)

    
        _activeDirectoryManager = activeDirectoryManager;
        _userRepository = new Repository<ApplicationUser>(context);
        _applicationRepository = new Repository<Application>(context);
        _roleRepository = new Repository<Role>(context);
    

    #endregion


    #region IUserManager

    public ApplicationUser AddUser(string username, List<string> applicationIds, bool isAdmin = false)
    
        //Get the environments in the list of environmentIds
        var applications = _applicationRepository.Get(e => applicationIds.Contains(e.Id)).ToList();

        //Get the user from AD
        var user = _activeDirectoryManager.GetUser(username);

        //set the Id
        user.Id = Guid.NewGuid().ToString();

        //add the environments to the user
        applications.ForEach(x =>
        
            user.Applications.Add(x);
        );

        //if the user is an admin - retrieve the role and add it to the user
        if (isAdmin)
        
            var role = _roleRepository.Get(r => r.Name == "admin").FirstOrDefault();
            if (role != null)
            
                user.Roles.Add(role);
            
        

        //insert and save
        _userRepository.Insert(user);
        _userRepository.Save();

        //return the user
        return user;

    

//removed for brevity

我的单元测试类:

[TestClass]
public class UserManagerUnitTest

    private readonly Mock<IActiveDirectoryManager> _adManager;
    private readonly IUserManager _userManager;
    private readonly Mock<IRepository<Application>> _applicationRepository;
    private readonly Mock<IRepository<ApplicationUser>> _userRepository;
    private readonly Mock<IRepository<Role>> _roleRepository;


    public UserManagerUnitTest()
    
        var context = new Mock<AppDbContext>();
        _adManager = new Mock<IActiveDirectoryManager>();

        _applicationRepository = new Mock<IRepository<Application>>();
        _userRepository = new Mock<IRepository<ApplicationUser>>();
        _roleRepository = new Mock<IRepository<Role>>();

        _userManager = new UserManager(context.Object, _adManager.Object);

    

    [TestMethod]
    [TestCategory("AddUser"), TestCategory("Unit")]
    public void AddUser_ValidNonAdmin_UserIsAdded()
    
        #region Arrange

        string username = "testUser";
        List<string> applicationIds = new List<string>() "1", "2", "3";

        _applicationRepository.Setup(x => x.Get(It.IsAny<Expression<Func<Application, bool>>>(), 
            It.IsAny<Func<IQueryable<Application>, IOrderedQueryable<Application>>>(), It.IsAny<string>()))
            .Returns(new List<Application>());

        _adManager.Setup(x => x.GetUser(It.IsAny<string>())).Returns(new ApplicationUser());


        #endregion

        #region Act

        var result = _userManager.AddUser(username, applicationIds, false);

        #endregion

        #region Assert
        Assert.IsNotNull(result);
        Assert.IsFalse(result.IsAdmin);
        #endregion
    


最后是存储库接口:

    public interface IRepository<TEntity> where TEntity : class

    IEnumerable<TEntity> Get(Expression<Func<TEntity, bool>> filter = null,
        Func<IQueryable<TEntity> , IOrderedQueryable<TEntity>> orderBy = null, string includeProperties = "");

    TEntity GetById(object id);
    void Insert(TEntity entity);
    void Delete(object id);
    void Delete(TEntity entityToDelete);
    void Update(TEntity entityToUpdate);
    void Save();


及实施:

public class Repository<TEntity> : IRepository<TEntity> where TEntity : class 

    private readonly AppDbContext _context;
    internal DbSet<TEntity> DbSet;

    public Repository(AppDbContext context)
    
        _context = context;
        DbSet = _context.Set<TEntity>();
    

    public virtual IEnumerable<TEntity> Get(Expression<Func<TEntity, bool>> filter = null,
        Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null, string includeProperties = "")
    
        IQueryable<TEntity> query = DbSet;

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

        foreach (var prop in includeProperties.Split(new char[] ',', StringSplitOptions.RemoveEmptyEntries))
        
            query = query.Include(prop);
        

        if (orderBy != null)
        
            return orderBy(query).ToList();
        
        else
        
            return query.ToList();
        


    

    public virtual TEntity GetById(object id)
    
        return DbSet.Find(id);
    

    public virtual void Insert(TEntity entity)
    
        DbSet.Add(entity);
    

    public virtual void Delete(object id)
    
        TEntity entityToDelete = DbSet.Find(id);
        Delete(entityToDelete);
    

    public void Get(Expression<Func<Application, bool>> expression, Func<IQueryable<Application>> func, IOrderedQueryable<Application> orderedQueryable)
    
        throw new NotImplementedException();
    

    public virtual void Delete(TEntity entityToDelete)
    
        if (_context.Entry(entityToDelete).State == EntityState.Detached)
        
            DbSet.Attach(entityToDelete);
        
        DbSet.Remove(entityToDelete);
    

    public virtual void Update(TEntity entityToUpdate)
    
        DbSet.Attach(entityToUpdate);
        _context.Entry(entityToUpdate).State = EntityState.Modified;
    

    public void Save()
    
        _context.SaveChanges();
    

我的问题在于模拟 IRepository&lt;Application&gt;

            _applicationRepository.Setup(x => x.Get(It.IsAny<Expression<Func<Application, bool>>>(), 
            It.IsAny<Func<IQueryable<Application>, IOrderedQueryable<Application>>>(), It.IsAny<string>()))
            .Returns(new List<Application>());

出于某种原因 - 使用的是实际方法,而不是 Moq 中的覆盖代理。当测试执行时 - 我在存储库的 Get 方法上得到一个空引用 - 特别是在 query = DbSet:

 public Repository(AppDbContext context)
    
        _context = context;
        DbSet = _context.Set<TEntity>();
    

    public virtual IEnumerable<TEntity> Get(Expression<Func<TEntity, bool>> filter = null,
        Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null, string includeProperties = "")
    
        IQueryable<TEntity> query = DbSet;  **//null here because db should be** mocked

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

我正在尝试仅测试 UserManager 实现 - 而不是存储库实现。

设置此测试的正确方法是什么?

【问题讨论】:

【参考方案1】:

问题是您在 UserManager 的构造函数中传递 AppDbContext,这使得它依赖于它。该类反过来创建存储库的内部实例,因此总是使用具体类:

public UserManager(AppDbContext context, IActiveDirectoryManager activeDirectoryManager)

    _activeDirectoryManager = activeDirectoryManager;
    _userRepository = new Repository<ApplicationUser>(context);
    _applicationRepository = new Repository<Application>(context);
    _roleRepository = new Repository<Role>(context);

您应该抽象出存储库的创建并修改构造函数,以便它采用基于接口的实例:

public UserManager(IRepository<ApplicationUser> userRepository, IRepository<Application> applicationRepository, IRepository<Role> roleRepository, IActiveDirectoryManager activeDirectoryManager)

    _activeDirectoryManager = activeDirectoryManager;
    _userRepository = userRepository;
    _applicationRepository = applicationRepository;
    _roleRepository = roleRepository;

通过这种方式,您可以抽象出存储库,以便使用您的模拟而不是真正的类。

【讨论】:

这就是我最初拥有它的方式。但是我遇到了保存问题,因为存储库都有不同的上下文实例。我正在使用 autofac,所以我更改了 UserManager 注册以使用我的数据库上下文的相同实例构造每个存储库 实例。把它清理干净了。谢谢。 好吧,如果是这样的话,你应该退后一步,重新审视你的设计。存储库之间不应存在依赖关系。如果您在保存时遇到问题,这可能意味着您有一些应该重构的重叠。

以上是关于使用 UnitOfWork 模拟上下文和存储库的主要内容,如果未能解决你的问题,请参考以下文章

UnitOfWork + Repository 模式和实体框架模拟

如何使用通用存储库实现 ADO.NET?

如何解决 Dapper - UnitOfWork 事务错误

无法模拟unitOfWork和没有服务接口

通用存储库、工作单元、Unity 的架构问题

如何使用带有代码优先实体框架的 UnitOfWork 模式防止重复条目?