利用Mocking Framework 单元测试Entity Framework
一、前言
在实际编写程序时,往往需要与数据库打交道,在单元测试中直接使用数据库又显得太重,如果可以方便的编写一些测试数据,这样更易于检测功能。如何模拟数据库行为便是本篇的主题。微软有教程说明Moq Entity Framework,需注意的是EF的版本必须是6以上。但在这篇教程中是直接使用DbContext,而自己的应用程序中都是用UnitOfWork模式。经过修改后也可以实现类似功能。
二、参考文献
https://msdn.microsoft.com/en-us/data/dn314429
三、采用UnitOfWork模式管理数据库
UnitOfWork
public interface IDomainUnitOfWork : IDisposable { DbContext Db { get; } //ImsDbContext dbContext { get; } Task SaveChangesClientWinAsync(); Task SaveChangesDataBaseWinAsync(); void SaveChangesClientWin(); void SaveChangesDataBaseWin(); }
Repository接口
public interface IDomainRepositoryAsync<T> where T : class { //Async Task<List<T>> GetAllAsync(Expression<Func<T, bool>> predicate = null); Task<T> SingleAsync(object primaryKey); Task<T> SingleOrDefaultAsync(object primaryKey); Task<bool> IsExistsAsync(Expression<Func<T, bool>> predicate = null); //同步 IQueryable<T> GetAll(Expression<Func<T, bool>> predicate = null); T Single(object primaryKey); T SingleOrDefault(object primaryKey); bool IsExist(Expression<Func<T, bool>> predicate = null); void Add(T entity); void Update(T entity); void Delete(T entity); }
Repository实现
public class DomainRepositoryAsync<T> : IDomainRepositoryAsync<T> where T : class { private readonly IDomainUnitOfWork _unitOfWork; internal DbSet<T> dbSet; public DomainRepositoryAsync(IDomainUnitOfWork unitOfWork) { if (unitOfWork == null) throw new ArgumentNullException("unitOfWork"); this._unitOfWork = unitOfWork; this.dbSet = _unitOfWork.Db.Set<T>(); } public void Add(T entity) { dynamic obj = dbSet.Add(entity); } public void Delete(T entity) { if (_unitOfWork.Db.Entry(entity).State == EntityState.Detached) { dbSet.Attach(entity); } dynamic obj = dbSet.Remove(entity); } public async Task<List<T>> GetAllAsync(Expression<Func<T, bool>> predicate = null) { if (predicate != null) { return await this.dbSet.Where(predicate).ToListAsync(); } return await this.dbSet.ToListAsync(); } public async Task<bool> IsExistsAsync(Expression<Func<T, bool>> predicate = null) { var result = false; if (predicate == null) { return result; } var query = await this.dbSet.Where(predicate).FirstOrDefaultAsync(); result = query == null ? false : true; return result; } /// <summary> /// 如果没有找到指定键元素,抛出异常. /// </summary> /// <param name="primaryKey">The primary key.</param> /// <returns>Task<T>.</returns> /// <exception cref="System.Collections.Generic.KeyNotFoundException"></exception> public async Task<T> SingleAsync(object primaryKey) { T dbResult = null; dbResult = await dbSet.FindAsync(primaryKey); if (dbResult == null) { throw new KeyNotFoundException(); } return dbResult; } public async Task<T> SingleOrDefaultAsync(object primaryKey) { var dbResult = await dbSet.FindAsync(primaryKey); return dbResult; } public void Update(T entity) { this.dbSet.Attach(entity); _unitOfWork.Db.Entry(entity).State = EntityState.Modified; } public IQueryable<T> GetAll(Expression<Func<T, bool>> predicate = null) { if (predicate != null) { return this.dbSet.Where(predicate); } return this.dbSet; } public T Single(object primaryKey) { T dbResult = null; dbResult = dbSet.Find(primaryKey); if (dbResult == null) { throw new KeyNotFoundException(); } return dbResult; } public T SingleOrDefault(object primaryKey) { var dbResult = dbSet.Find(primaryKey); return dbResult; } public bool IsExist(Expression<Func<T, bool>> predicate = null) { var result = false; if (predicate == null) { return result; } var query = this.dbSet.Where(predicate).FirstOrDefault(); result = query == null ? false : true; return result; } }
DbContext
public class DurationDbContext : DbContext { public virtual DbSet<Department> Departments { get; set; } public virtual DbSet<Duration> Durations { get; set; } }
四、配置UnitTest
1、首先用Nuget安装moq
2、注意在DomainRepositoryAsync中有一个DbSet<T> dbSet,需要Moq的就是该类型,并且在DbContext中的必须加"virtual“关键字。
3、Moq代码
var unitOfWork = new Mock<IDomainUnitOfWork>(); var mockDepartment = SetupMockDbSet<Department>(DepartmentList); var mockDuration = SetupMockDbSet<Duration>(DurationList); unitOfWork.Setup(m => m.Db.Set<Department>()).Returns(mockDepartment.Object); unitOfWork.Setup(m => m.Db.Set<Duration>()).Returns(mockDuration.Object);
在教程中说到了如何处理异步的查询操作,教程很详细,此处便不再重复,直接将代码Copy到单元测试工程中即可,再将重复的代码作为一个方法SetupMockDBSet。
public static Mock<DbSet<T>> SetupMockDbSet<T>(List<T> dataList) where T : class { var data = dataList.AsQueryable(); var mockSet = new Mock<DbSet<T>>(); mockSet.As<IDbAsyncEnumerable<T>>() .Setup(m => m.GetAsyncEnumerator()) .Returns(new TestDbAsyncEnumerator<T>(data.GetEnumerator())); mockSet.As<IQueryable<T>>() .Setup(m => m.Provider) .Returns(new TestDbAsyncQueryProvider<T>(data.Provider)); mockSet.As<IQueryable<T>>().Setup(m => m.Expression).Returns(data.Expression); mockSet.As<IQueryable<T>>().Setup(m => m.ElementType).Returns(data.ElementType); mockSet.As<IQueryable<T>>().Setup(m => m.GetEnumerator()).Returns(data.GetEnumerator()); return mockSet; }
注意在配置UnitOfWork时,用Setup中是对IUnitOfWork的Db进行设置。接下来的实现方式与教程相同,不再重复。