'How to mock Repository/Unit Of Work
In my app I have generic repository connected to controller through UnitOfWork. I want to unit test my app. To make this I need to mock db connection. Can you tell me what should do? Mock repo? Mock repo and UnitOfWork? I'll be gratefull for any code snippets/suggestions. Here my 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;
}
}
and 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);
}
}
sample controller:
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);
}
}
Solution 1:[1]
Unfortunately your GenericRepository<T>
is tightly coupled with your context, and your UnitOfWork implementation if tightly coupled with your repositories. This makes it impossible to mock it.
You have to introduce loosely coupling:
- Add an interface
IRepository<T>
, and implement this with yourGenericRepository<T>
class - Add in interface
IUnitOfWork
and implement this with yourUnitOfWork
class - The
IUnitOfWork
interface only refers toIRepository<T>
, not toGenericRepository<T>
- Update your controller constructors to expect an
IUnitOfWork
instead of a UnitOfWork. Best would be to inject the repositories in you unit of work, but that would mean a lot of constructor-parameters, and you would have an instance of it already while you might not be using it. The solution to this in my mind would be aIRepositoryFactory
(with a corresponding implementation), which would allow you to create a specific repository on demand. The factory would have a genericCreate
method to create a generic repository. This factory could then be injected in your unit-of-work implementation.
Now you can mock every part of you unit of work and/or repositories.
Update
I've removed the repository-factory in the text above, and the code below. The reason for this, is that when I tried to create pseudo-code I had a bit of trouble to pass the context to the generic-repository, since the repository-factory didn't know this object. And, since the unit-of-work and the generic-repository are both tightly coupled anyway (since they share the context-object), I've come up with the following solution:
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
}
Solution 2:[2]
As noted, you have high cohesion between your classes.
The preferable method is to break this cohesion (introducing isolation) by using interfaces. However, you can also use Microsoft's faking framework to create shims. Shims allow you to divert the behavior of methods and properties of an object to create simulations of concrete types.
Using shims to isolate your application from other assemblies for unit testing
Shim types are one of two technologies that the Microsoft Fakes Framework uses to let you easily isolate components under test from the environment. Shims divert calls to specific methods to code that you write as part of your test. Many methods return different results dependent on external conditions, but a shim is under the control of your test and can return consistent results at every call. This makes your tests much easier to write.
Use shims to isolate your code from assemblies that are not part of your solution. To isolate components of your solution from each other, we recommend that you use stubs.
As of writing, you have already accepted an answer. However, the faking framework is darkest wizard magic and should be explored. Using shims will give you a good eye for where an interface is required.
Solution 3:[3]
Thanks to @sonmt and @qujck here is an implementation. I had some gaps to fill in. Primarily based off @sonmt's solution. But this will run (F5) and is end-to-end. Hope it helps someone!
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | |
Solution 2 | Gusdor |
Solution 3 | Robert Green MBA |