如何模拟存储库/工作单元

在我的应用程序中,我通过UnitOfWork连接到控制器的通用存储库。 我想对我的应用进行unit testing。 为此,我需要模拟数据库连接。 你能告诉我应该怎么做吗? 模拟回购? 模拟回购和UnitOfWork? 我会感激任何代码片段/建议。 在这里我的回购:

public class GenericRepository where TEntity : class { internal EquipmentEntities context; internal DbSet dbSet; public GenericRepository(EquipmentEntities context) { this.context = context; this.dbSet = context.Set(); } public virtual IEnumerable Get( List<Expression<Func>> filter, Func<IQueryable, IOrderedQueryable> orderBy = null, int? Page=0, params Expression<Func>[] included) { IQueryable 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 RoleRepository; private GenericRepository StorageRepository; private GenericRepository DeviceRepository; private GenericRepository DeviceInstanceRepository; private GenericRepository DeviceUsageRepository; private GenericRepository UserRepository; public GenericRepository roleRepository { get { if (this.RoleRepository == null) { this.RoleRepository = new GenericRepository(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>> where = new List<Expression<Func>>(); 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); } } 

不幸的是,您的GenericRepository与您的上下文紧密耦合,而您的UnitOfWork实现与您的存储库紧密耦合。 这使得它无法模仿它。

你必须引入松耦合:

  • 添加一个接口IRepository ,并使用您的GenericRepository类实现它
  • 添加IUnitOfWork接口并使用UnitOfWork类实现此function
  • IUnitOfWork接口仅引用IRepository而不引用GenericRepository
  • 更新控制器构造函数以期望IUnitOfWork而不是UnitOfWork。
  • 最好的方法是在您的工作单元中注入存储库,但这意味着很多构造函数参数, 并且在您可能没有使用它时,您将拥有它的实例。 在我看来,这个解决方案将是一个IRepositoryFactory (具有相应的实现),它允许您按需创建特定的存储库。 工厂将有一个通用的Create方法来创建一个通用的存储库。 然后可以将此工厂注入您的工作单元实施中。

现在,您可以模拟工作单元和/或存储库的每个部分。

更新

我在上面的文本中删除了存储库工厂,以及下面的代码。 原因是,当我尝试创建伪代码时,我将上下文传递给generics存储库时遇到了一些麻烦,因为存储库工厂不知道这个对象。 而且,由于工作单元通用存储库都是紧密耦合的(因为它们共享上下文对象),我提出了以下解决方案:

 public interface IRepository where TEntity: class { // Your methods } public class GenericRepository : IRepository where TEntity : class { public GenericRepository(EquipmentEntities context) { // Your constructor } // Your implementation } public interface IUnitOfWork : IDisposable { IRepository RoleRepository { get; } IRepository StorageRepository { get; } // etc void Save(); } public class UnitOfWork : IUnitOfWork { public UnitOfWork () { this.context = new EquipmentEntities (); } private EquipmentEntities context = null; private IRepository roleRepository; public IRepository RoleRepository { get { if (this.roleRepository == null) { this.roleRepository = new GenericRepository(context); } return this.roleRepository; } } // etc... other repositories // etc... your implementation for Save and Dispose } 

如上所述,您的课程之间具有很高的凝聚力。

优选的方法是通过使用界面来破坏这种内聚(引入隔离)。 但是,您也可以使用Microsoft的伪造框架来创建填充程序 。 垫片允许您转移对象的方法和属性的行为,以创建具体类型的模拟。

使用填充程序将应用程序与其他程序集隔离以进行unit testing

填充类型是Microsoft Fakes Framework使用的两种技术之一,可让您轻松地将测试中的组件与环境隔离。 垫片将调用转移到特定方法,以编写您作为测试的一部分编写的代码。 许多方法根据外部条件返回不同的结果,但是垫片在测试的控制之下,并且可以在每次调用时返回一致的结果。 这使您的测试更容易编写。

使用填充程序将代码与不属于解决方案的程序集隔离开来。 要将解决方案的组件彼此隔离,我们建议您使用存根。

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

在撰写时,您已经接受了答案。 然而,假装框架是最黑暗的巫师魔法,应该进行探索。 使用垫片可以让您更好地了解需要接口的位置。