【问题标题】:How to mock Repository/Unit Of Work如何模拟存储库/工作单元
【发布时间】:2022-04-23 15:30:31
【问题描述】:

在我的应用程序中,我有通过 UnitOfWork 连接到控制器的通用存储库。我想对我的应用程序进行单元测试。为此,我需要模拟数据库连接。 你能告诉我应该怎么做吗?模拟回购?模拟回购和 UnitOfWork?对于任何代码 sn-ps/建议,我将不胜感激。 这是我的回购:

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;
    }
}

和工作单元:

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);
    }
}

【问题讨论】:

  • 您应该测试每个存储库并测试您的 UnitOfWork 类,以确保它使用正确的数据库连接创建正确的存储库。为此,您应该模拟 EF 上下文 - 您可以将其简化为设置一个虚拟单元测试数据库并连接到该数据库,或者使用内存数据库,如 Effort

标签: c# unit-testing mocking


【解决方案1】:

不幸的是,您的 GenericRepository&lt;T&gt; 与您的上下文紧密耦合,如果您的 UnitOfWork 实现与您的存储库紧密耦合。这使得它无法模拟它。

你必须引入松散耦合:

  • 添加一个接口IRepository&lt;T&gt;,并用你的GenericRepository&lt;T&gt;类实现它
  • 添加接口 IUnitOfWork 并使用您的 UnitOfWork 类实现它
  • IUnitOfWork接口只指IRepository&lt;T&gt;不是GenericRepository&lt;T&gt;
  • 更新您的控制器构造函数以期望 IUnitOfWork 而不是 UnitOfWork。
  • 最好在你的工作单元中注入存储库,但这意味着很多构造函数参数,并且你可能已经有了它的实例,而你可能没有正在使用它。在我看来,解决方案是IRepositoryFactory(带有相应的实现),它允许您按需创建特定的存储库。工厂将有一个通用的Create 方法来创建一个通用的存储库。然后可以将该工厂注入到您的工作单元实现中。

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

更新

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

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
}

【讨论】:

  • 非常感谢!我有2个小问题。你说Update your controller constructors to expect an IUnitOfWork instead of a UnitOfWork. 但我不知道如何改变它,因为我没有构造器。包括样品控制器。而且我在 IUnit/Unit 中有很多不一致的可访问性错误:(
  • 您应该在创建对象时使用 DI 框架来注入依赖项。检查stackoverflow.com/a/16085891/261050
  • Error 1 Inconsistent accessibility: property type 'magazyn.DAL.IRepository&lt;magazyn.Models.Role&gt;' is less accessible than property 'magazyn.DAL.IUnitOfWork.RoleRepository' 用于接口中的每个 repo
  • 看起来你的接口IRepository&lt;T&gt;不是public
  • 是的,在修复它时我发现并修复了另一个错误。谢谢!
【解决方案2】:

如前所述,您的班级之间具有很高的凝聚力。

最好的方法是通过使用接口来打破这种内聚(引入隔离)。但是,您也可以使用 Microsoft 的伪装框架来创建 shims。 Shims 允许您转移对象的方法和属性的行为,以创建具体类型的模拟。

使用 shim 将您的应用程序与其他程序集隔离以进行单元测试

Shim 类型是 Microsoft Fakes 的两种技术之一 框架用于让您轻松地将被测组件与 环境。 Shims 将调用转移到特定方法以进行编码 您将写作作为测试的一部分。许多方法返回不同的结果 取决于外部条件,但垫片是在控制之下 您的测试,并且可以在每次通话时返回一致的结果。这使得 您的测试更容易编写。

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

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

在撰写本文时,您已经接受了一个答案。然而,伪装框架是最黑暗的巫术,应该被探索。使用 shim 可以让您清楚地了解需要接口的位置。

【讨论】:

  • 谢谢。我在 asp.net 网站上使用教程制作了这个。现在我很遗憾他们提出了这种需要重构一半代码以使其可测试的方法:(感谢Shims的建议,但它需要VS Ultimate
【解决方案3】:

感谢@sonmt@qujck,这是一个实现。我有一些空白要填补。主要基于@sonmt 的解决方案。但这将运行 (F5) 并且是端到端的。希望它可以帮助某人!

https://github.com/DocGreenRob/TestableGenericRepository

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-05-10
    • 1970-01-01
    • 1970-01-01
    • 2012-12-25
    • 2023-02-24
    • 2013-04-23
    相关资源
    最近更新 更多