Reputation: 220
I wrote an application using repository pattern and unit of work, it is also using entity framework in order to interact with db (mssql in my case).
Right now I have two projects in my application: 1-DAL which stands for data access layer, and 2-BLL which stands for bisnel logic layer. I have my unitofwork, repository (I have one generic repo for all classes), and basic data access controllers(like GetAll, or GetById, Edit, Delete, and Create) in DAL project. Also, I have DTO mapper, and more "logic" controllers in BLL project.
I need some explanation on writing tests for this, like: 1) Do I have to write tests for data access layer? 2) If I have to test the data access layer, do I have to write tests for only controller, or for repository as well (as they are pretty much the same)? 3) Do I have to seed the database in the test cases themselves, or use the "pre seeded db"? 4) Do I have to write tests for mapper (I am using Mapper extension) p.s.: If there is an article for this theme, I'll be grateful.
I will attach some of my code bellow: Generic repository:
public class GenericRepository<TEntity> where TEntity : class
{
internal WarehouseContext context;
internal DbSet<TEntity> dbset;
public GenericRepository(WarehouseContext context)
{
this.context = context;
this.dbset = context.Set<TEntity>();
}
public virtual IEnumerable<TEntity> GetAll()
{
return dbset;
}
public virtual TEntity GetById(Guid id)
{
return dbset.Find(id);
}
public virtual void Add(TEntity entity)
{
dbset.Add(entity);
}
public virtual void Delete(Guid id)
{
TEntity entityToDelete = dbset.Find(id);
dbset.Remove(entityToDelete);
}
public virtual void Edit(TEntity entityToUpdate)
{
dbset.Attach(entityToUpdate);
context.Entry(entityToUpdate).State = EntityState.Modified;
}
}
DAL controller for CategoryClass:
private readonly UnitOfWork _unitOfWork;
public CategoryController()
{
_unitOfWork = new UnitOfWork();
}
public List<Category> GetCategories()
{
return _unitOfWork.CategoryRepository.GetAll().ToList();
}
public Category GetCategoryById(Guid id)
{
return _unitOfWork.CategoryRepository.GetById(id);
}
public void AddCategory(Category category)
{
_unitOfWork.CategoryRepository.Add(category);
_unitOfWork.Save();
}
public void EditCategory(Category category)
{
_unitOfWork.CategoryRepository.Edit(category);
_unitOfWork.Save();
}
public void DeleteCategory(Guid id)
{
_unitOfWork.CategoryRepository.Delete(id);
_unitOfWork.Save();
}
Example of what I have in BLL controller for CategoryClass:
private readonly CategoryController _categoryController;
private readonly IMapper _mapper;
public CategoryLogicContoller()
{
_categoryController = new CategoryController();
var profile = new CategoryProfile();
_mapper = profile.CategoryMapper;
}
public List<CategoryDTO_short> GetAllCategories_shortDescription()
{
var categories = _categoryController.GetCategories().ToList();
var categoriesDTO = new List<CategoryDTO_short>();
foreach (var category in categories)
{
var categoryDTO = _mapper.Map<Category, CategoryDTO_short>(category);
categoriesDTO.Add(categoryDTO);
}
return categoriesDTO;
}
I am also attaching the screenshot of my app architecture:
Thanks for answers!
Upvotes: 0
Views: 3406
Reputation: 2020
What a coincidence, I implemented unit testing for this pattern a few days ago.
The UnitOfWork
should use a generic repository, so you can define a repository for Entity Framework, and a repository that stores data in a list, e.g. MemoryRepository
. This way the unit testing is not depending on the database, or actually, also not depending on Entity Framework at all.
Since the code is quite long, I have added a few excerpts and removed a lot of code.
The unit of work receives an instance of DB context and a factory that creates instances of repositories, with the given instance of DB context.
The unit of work will receive a factory instead of a repository. The idea is to give it a different factory when unit testing.
public class UnitOfWork
{
IDbContext _db { get; }
IRepositoryFactory _repositoryFactory { get; }
IRepository<Category> _categoryRepository { get; set; } = null!;
public UnitOfWork(IDbContext db, IRepositoryFactory repositoryFactory)
{
_db = db;
_repositoryFactory = repositoryFactory;
}
public IRepository<Question> Questions
{
get => (_categoryRepository ??= _repositoryFactory.CreateInstance<Category>(_db));
}
public void Commit()
{
_db.Commit();
}
public async Task CommitAsync()
{
await _db.CommitAsync();
}
}
The interface uses an IKey
interface, which says that the implementation should have a Guid Id { get; set; }
defined on it. I have added this to all the entities, so the memory repository has an index for the dictionary.
public interface IRepositoryFactory
{
public IRepository<TEntity> CreateInstance<TEntity>(IDbContext db) where TEntity : class, IKey;
}
This factory will create an instance of the EF repository, which is used in the application, and added to the DI container in the startup of the application.
public class EFRepositoryFactory : IRepositoryFactory
{
public IRepository<TEntity> CreateInstance<TEntity>(IDbContext db) where TEntity : class, IKey
{
return new EFRepository<TEntity>((FetchDbContext)db);
}
}
This factory will create an instance of the memory repository, that is used in unit testing.
public class MemoryRepositoryFactory : IRepositoryFactory
{
public IRepository<TEntity> CreateInstance<TEntity>(IDbContext db) where TEntity : class, IKey
{
return new MemoryRepository<TEntity>();
}
}
This is the repository that interacts with the database.
public class EFRepository<TEntity> : IRepository<TEntity> where TEntity : class, IKey
{
internal FetchDbContext _db;
internal DbSet<TEntity> _entities;
public EFRepository(FetchDbContext context)
{
_db = context;
_entities = context.Set<TEntity>();
}
public virtual IEnumerable<TEntity> Get(
Expression<Func<TEntity, bool>>? filter = null,
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>>? orderBy = null,
int skip = -1,
int take = -1,
string includeProperties = "")
{
IQueryable<TEntity> query = _entities;
if (filter != null)
{
query = query.Where(filter);
}
foreach (var includeProperty in includeProperties.Split
(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
{
query = query.Include(includeProperty.Trim());
}
if (orderBy != null)
{
query = orderBy(query);
}
if (skip >= 0)
{
query = query.Skip(skip);
}
if (take >= 0)
{
query = query.Take(take);
}
return query.ToList();
}
public virtual async Task<IEnumerable<TEntity>> GetAsync(
Expression<Func<TEntity, bool>>? filter = null,
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>>? orderBy = null,
int skip = -1,
int take = -1,
string includeProperties = "")
{
...
}
public virtual TEntity GetById(object id)
{
var result = _entities.Find(id);
if (result is null)
{
throw new KeyNotFoundException(nameof(id));
}
return result;
}
public virtual async Task<TEntity> GetByIdAsync(object id)
{
...
}
...
}
This repository saves the data in a dictionary instead of a database;
public class MemoryRepository<TEntity> : IRepository<TEntity> where TEntity : class, IKey
{
// Dictionary
Dictionary<Guid, TEntity> _entities = new();
public virtual IEnumerable<TEntity> Get(
Expression<Func<TEntity, bool>>? filter = null,
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>>? orderBy = null,
int skip = -1,
int take = -1,
string includeProperties = "")
{
IQueryable<TEntity> query = _entities.Values.AsQueryable();
if (filter != null)
{
query = query.Where(filter);
}
foreach (var includeProperty in includeProperties.Split
(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
{
query = query.Include(includeProperty);
}
if (orderBy != null)
{
return orderBy(query).ToList();
}
else
{
return query.ToList();
}
}
public virtual async Task<IEnumerable<TEntity>> GetAsync(
Expression<Func<TEntity, bool>>? filter = null,
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>>? orderBy = null,
int skip = -1,
int take = -1,
string includeProperties = "")
{
...
}
public virtual TEntity GetById(object id)
{
Guid guid = (Guid)id;
var result = _entities.Values.FirstOrDefault(e => e.Id == guid);
if (result is null)
{
throw new KeyNotFoundException(nameof(id));
}
return result;
}
public virtual async Task<TEntity> GetByIdAsync(object id)
{
...
}
public virtual void Insert(TEntity entity)
{
if (entity.Id == default)
{
entity.Id = Guid.NewGuid();
}
_entities[entity.Id] = entity;
}
...
}
I have implemented the following methods on the DbContext class, and defined them as an interface.
public interface IDbContext
{
void Commit();
Task CommitAsync();
}
public interface IRepository<TEntity> where TEntity : class, IKey
{
int Count(Expression<Func<TEntity, bool>>? filter = null);
Task<int> CountAsync(Expression<Func<TEntity, bool>>? filter = null);
void Delete(object id);
void Delete(TEntity entityToDelete);
Task DeleteAsync(object id);
IEnumerable<TEntity> Get(Expression<Func<TEntity, bool>>? filter = null,
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>>? orderBy = null, int skip = -1, int take = -1,
string includeProperties = "");
Task<IEnumerable<TEntity>> GetAsync(Expression<Func<TEntity, bool>>? filter = null,
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>>? orderBy = null, int skip = -1, int take = -1,
string includeProperties = "");
TEntity GetById(object id);
Task<TEntity> GetByIdAsync(object id);
void Insert(TEntity entity);
Task InsertAsync(TEntity entity);
void Update(TEntity entityToUpdate);
}
In the unit test we can define a unit of work in the following way:
var dbContext = new MockDbContext();
var repositoryFactory = new MemoryRepositoryFactory();
var unitOfWork = new UnitOfWork(dbContext, repositoryFactory);
And we will pass that mocked instance of DB context
private class MockDbContext : IDbContext
{
public void Commit()
{
return;
}
public async Task CommitAsync()
{
await Task.Delay(0); // Probably something better applies here
}
}
I use this to create instances of the services in the unit tests so I can test the business logic in the services.
Storing the entities in memory is a lot different than from a database. To give an example, if we have multiple nested entities, EF will create entities in the appropriate tables. However, this does not happen in the memory repository, so if you'd insert something in table A, and expect something in table B as well, then that will not be there!
If you have entities that derive from a common base entity, then this approach gives some issues to the reason above.
Hope this helps!
Upvotes: 1