如何模拟 Repository/Unit Of Work

11

我在我的应用程序中有一个通用仓库,通过UnitOfWork连接到控制器。我想对我的应用程序进行单元测试。为了做到这一点,我需要模拟数据库连接。请问我该怎么办?模拟仓库?还是同时模拟仓库和 UnitOfWork?如果能给出任何代码片段或建议,我将不胜感激。下面是我的Repo:

public class GenericRepository<TEntity> where TEntity : class
{
    internal EquipmentEntities context;
    internal DbSet<TEntity> dbSet;

    public GenericRepository(EquipmentEntities context)
    {
        this.context = context;
        this.dbSet = context.Set<TEntity>();
    }

    public virtual IEnumerable<TEntity> Get(
        List<Expression<Func<TEntity, bool>>> filter,
        Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
        int? Page=0,
        params Expression<Func<TEntity, object>>[] included)
    {

        IQueryable<TEntity> query = dbSet;

        foreach(var z in included)
        {
            query=query.Include(z);
        }
        if (orderBy != null)
        {
            query = orderBy(query);
            query = query.Skip((Page.Value - 1) * 30).Take(30);
        }
        if (filter != null)
        {
            foreach (var z in filter)
            {
                query = query.Where(z);
            }
        }
        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 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;
    }
}

以及UnitOfWork:

public class UnitOfWork {
    private EquipmentEntities context = new EquipmentEntities();
    private GenericRepository<Role> RoleRepository;
    private GenericRepository<Storage> StorageRepository;
    private GenericRepository<Device> DeviceRepository;
    private GenericRepository<DeviceInstance> DeviceInstanceRepository;
    private GenericRepository<DeviceUsage> DeviceUsageRepository;
    private GenericRepository<User> UserRepository;

    public GenericRepository<Role> roleRepository
    {
        get
        {
            if (this.RoleRepository == null)
            {
                this.RoleRepository = new GenericRepository<Role>(context);
            }
            return RoleRepository;
        }
    }

    /*
    * redundant code for other controllers
    */
    public void Save()
    {
        context.SaveChanges();
    }

    private bool disposed = false;

    protected virtual void Dispose(bool disposing)
    {
        if (!this.disposed)
        {
            if (disposing)
            {
                context.Dispose();
            }
        }
        this.disposed = true;
    }

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

示例控制器:

 public class UserController : Controller
{
    //private EquipmentEntities db = new EquipmentEntities();
    private UnitOfWork unitOfWork = new UnitOfWork();

    // GET: /User/
    public ActionResult Index(string Name, string Surname, int? Page, string submit)
    {
        List<Expression<Func<User, bool>>> where = new List<Expression<Func<User, bool>>>();
        if (!string.IsNullOrEmpty(Name))
        {
            where.Add(w => w.Name.Contains(Name));
        }
        if (!string.IsNullOrEmpty(Surname))
        {
            where.Add(w => w.Surname.Contains(Surname));
        }
        var users = unitOfWork.userRepository.Get(where, null, Page, u => u.Role);
        return View(users);
    }

    // GET: /User/Details/5
    public ActionResult Details(int? id)
    {
        if (id == null)
        {
            return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
        }
        User user = unitOfWork.userRepository.GetByID(id.Value);
        //User user = db.Users.Find(id);
        if (user == null)
        {
            return HttpNotFound();
        }
        return View(user);
    }

    // GET: /User/Create
    public ActionResult Create()
    {
        ViewBag.RoleId = new SelectList(unitOfWork.roleRepository.Get(null), "Id", "RoleName");
        return View();
    }

    // POST: /User/Create
    // To protect from overposting attacks, please enable the specific properties you want to bind to, for 
    // more details see http://go.microsoft.com/fwlink/?LinkId=317598.
    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Create([Bind(Include="Id,EmployeeNo,Name,Surname,ContactInfo,RoleId")] User user)
    {
        if (ModelState.IsValid)
        {
            unitOfWork.userRepository.Insert(user);
            unitOfWork.Save();
            return RedirectToAction("Index");
        }
        ViewBag.RoleId = new SelectList(unitOfWork.roleRepository.Get(null), "Id", "RoleName", user.RoleId);
        return View(user);
    }

    // GET: /User/Edit/5
    public ActionResult Edit(int? id)
    {
        if (id == null)
        {
            return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
        }
        User user = unitOfWork.userRepository.GetByID(id.Value);
        if (user == null)
        {
            return HttpNotFound();
        }
        ViewBag.RoleId = new SelectList(unitOfWork.roleRepository.Get(null), "Id", "RoleName", user.RoleId);
        return View(user);
    }

    // POST: /User/Edit/5
    // To protect from overposting attacks, please enable the specific properties you want to bind to, for 
    // more details see http://go.microsoft.com/fwlink/?LinkId=317598.
    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Edit([Bind(Include="Id,EmployeeNo,Name,Surname,ContactInfo,RoleId")] User user)
    {
        if (ModelState.IsValid)
        {
            unitOfWork.userRepository.Update(user);
            unitOfWork.Save();
            return RedirectToAction("Index");
        }
        ViewBag.RoleId = new SelectList(unitOfWork.roleRepository.Get(null), "Id", "RoleName", user.RoleId);
        return View(user);
    }

    // GET: /User/Delete/5
    public ActionResult Delete(int? id)
    {
        if (id == null)
        {
            return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
        }
        User user = unitOfWork.userRepository.GetByID(id.Value);
        if (user == null)
        {
            return HttpNotFound();
        }
        if (unitOfWork.deviceUsageRepository.Get(null).Where(w => w.UserId == id) != null)
        {
            ViewBag.Error = 1;
            ModelState.AddModelError("", "Nie można kasować uyztkownika z przypisanymi urządzeniami");

        }
        else
        {
            ViewBag.Error = 0;
        }
        return View(user);
    }

    // POST: /User/Delete/5
    [HttpPost, ActionName("Delete")]
    [ValidateAntiForgeryToken]
    public ActionResult DeleteConfirmed(int id)
    {
        User user = unitOfWork.userRepository.GetByID(id);
        unitOfWork.deviceUsageRepository.Delete(user);
        unitOfWork.Save();
        return RedirectToAction("Index");
    }

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            unitOfWork.Save();
        }
        base.Dispose(disposing);
    }
}

1
你应该测试每个Repository,还要测试你的UnitOfWork类以确保它使用正确的DB连接创建了正确的repositories。为此,你应该模拟EF上下文 - 你可以简单地设置一个虚拟的单元测试数据库并连接到它,或者使用像Effort这样的内存数据库。 - CodingIntrigue
3个回答

17

很不幸,你的GenericRepository<T>与你的上下文紧密耦合,并且你的UnitOfWork实现与你的repositories也是紧密耦合的。这使得它无法进行模拟。

你必须引入松散耦合:

  • 添加一个IRepository<T>接口,并使用你的GenericRepository<T>类来实现它
  • 添加一个IUnitOfWork接口,并使用你的UnitOfWork类来实现它
  • IUnitOfWork接口仅涉及IRepository<T>,而不涉及GenericRepository<T>
  • 更新你的控制器构造函数,让它们期望一个IUnitOfWork而不是UnitOfWork。
  • 最好的方法是将repositories注入到你的unit of work中,但这意味着会有很多构造函数参数,而且你可能已经有了它的实例,而你却可能没有使用它。我考虑的解决方案是使用IRepositoryFactory(以及对应的实现),它将允许你按需创建特定的repository。工厂将有一个泛型Create方法来创建一个泛型repository。这个工厂现在可以注入到你的unit-of-work实现中。

现在,你可以模拟你的unit of work和/或repositories的每个部分。

更新

我已经从上面的文本和下面的代码中删除了repository-factory。原因是,当我尝试创建伪代码时,我遇到了一些问题,因为repository-factory不知道这个对象的上下文。并且,既然unit-of-workgeneric-repository都紧密耦合(因为它们共享context-object),我想出了以下解决方案:

public interface IRepository<TEntity> where TEntity: class {
    // Your methods
}
public class GenericRepository<TEntity> : IRepository<TEntity> where TEntity : class {
    public GenericRepository<TEntity>(EquipmentEntities  context) {
        // Your constructor
    }

    // Your implementation
}

public interface IUnitOfWork : IDisposable {
    IRepository<Role> RoleRepository { get; }
    IRepository<Storage> StorageRepository { get; }
    // etc

    void Save();
}

public class UnitOfWork : IUnitOfWork {
    public UnitOfWork () {
        this.context = new EquipmentEntities ();
    }

    private EquipmentEntities context = null;

    private IRepository<Role> roleRepository;
    public IRepository<Role> RoleRepository { 
        get {
            if (this.roleRepository == null) {
                this.roleRepository = new GenericRepository<Role>(context);
            }
            return this.roleRepository;
        }
    }

    // etc... other repositories
    // etc... your implementation for Save and Dispose
}

非常感谢!我有两个小问题。你说“更新你的控制器构造函数,使其期望一个IUnitOfWork而不是UnitOfWork。”但是我不知道如何更改,因为我没有构造函数。附带了示例控制器。而且,在IUnit/Unit中我有很多不一致的可访问性错误 :( - szpic
1
在创建对象时,您应该使用 DI 框架来注入依赖项。请查看 https://dev59.com/82Qo5IYBdhLWcg3wbe1K#16085891 - Maarten
错误1:不一致的可访问性:属性类型'magazyn.DAL.IRepository<magazyn.Models.Role>'比属性'magazyn.DAL.IUnitOfWork.RoleRepository'更不可访问 - szpic
1
看起来你的接口 IRepository<T> 不是 public 的。 - Maarten
是的,就是这样,在修复它的过程中,我发现并修复了另一个错误。谢谢! - szpic

2

如前所述,你的类之间具有高内聚性。

优先的方法是通过使用接口来打破这种内聚性(引入隔离)。但是,你也可以使用微软的模拟框架来创建shims。 Shims允许你将对象的方法和属性的行为重定向到创建具体类型的模拟。

使用shims来隔离应用程序以进行单元测试

Shim类型是Microsoft Fakes Framework使用的两种技术之一,它们让你轻松地将测试组件与环境隔离开。 shims将对特定方法的调用转向为你作为测试的一部分编写的代码。许多方法返回不同的结果,取决于外部条件,但是shim在你的测试控制下,并且可以在每次调用时返回一致的结果。这使得你的测试更加容易编写。

使用shims来将代码与不属于你解决方案的程序集隔离开来进行测试。要将解决方案的组件彼此隔离,请使用stubs。

http://msdn.microsoft.com/en-us/library/hh549176.aspx

截至目前为止,你已经接受了一个答案。但是,模拟框架是最黑暗的巫术,并值得探索。使用shims将有助于你判断何时需要使用接口。


谢谢。我是按照asp.net网站上的教程制作的。现在我非常难过,因为他们建议使用这种方法需要重构一半的代码才能进行测试:( 谢谢你提供Shims的建议,但它需要VS ultimate。 - szpic

0

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