剛用上Moq,就用它解決了一個IUnitOfWork的mock問題,在這篇博文中記錄一下。
開發場景
Application服務層BlogCategoryService的實現代碼如下:
public class BlogCategoryService : IBlogCategoryService
{
private IBlogCategoryRepository _blogCategoryRepository;
public BlogCategoryServiceImp(IBlogCategoryRepository blogCategoryRepository)
{
_blogCategoryRepository = blogCategoryRepository;
}
public async Task<IList<BlogCategory>> GetCategoriesAsync(int blogId)
{
return await _blogCategoryRepository.GetCategories(blogId).ToListAsync();
}
}
這里用到了Entity Framework中System.Data.Entity命名空間下的ToListAsync()擴展方法。
Repository層BlogCategoryRepository的實現代碼如下:
public class BlogCategoryRepository : IBlogCategoryRepository
{
private IQueryable<BlogCategory> _categories;
public BlogCategoryRepository(IUnitOfWork unitOfWork)
{
_categories = unitOfWork.Set<BlogCategory>();
}
public IQueryable<BlogCategory> GetCategories(int blogId)
{
return _categories.Where(c => c.BlogId == blogId);
}
}
這里在BlogCategoryRepository的構造函數中通過IUnitOfWork接口獲取BlogCategory的數據集。
在單元測試中一開始是這樣用Moq對IUnitOfWork接口進行mock的——讓IUnitOfWork.Set()方法直接返回IQueryable類型的BlogCategory集合,代碼如下:
[Fact]
public async Task GetCategoriesTest()
{
var blogCategories = new List<BlogCategory>()
{
new BlogCategory { BlogId = 1, Active = true, CategoryId = 1, Title = "C#" },
new BlogCategory { BlogId = 1, Active = false, CategoryId = 2, Title = "ASP.NET Core" }
}.AsQueryable();
var mockUnitOfWork = new Mock<IUnitOfWork>();
mockUnitOfWork.Setup(u => u.Set<BlogCategory>()).Returns(blogCategories);
_categoryService = new BlogCategoryServiceImp(new BlogCategoryRepository(mockUnitOfWork.Object));
var actual = await _categoryService.GetCategoriesAsync(1);
Assert.Equal(2, actual.Count());
actual.ToList().ForEach(c => Assert.Equal(1, c.BlogId));
}
遇到問題
運行單元測試時,卻出現下面的錯誤:
The source IQueryable doesn't implement IDbAsyncEnumerable<BlogCategory>.
Only sources that implement IDbAsyncEnumerable can be used for Entity Framework asynchronous operations.
出現這個錯誤是由於在BlogCategoryService中用到了EF的ToListAsync()擴展方法,使用這個擴展方法需要實現IDbAsyncEnumerable相關接口,而通過List
解決問題
那如何mock呢?比想象中復雜得多,幸好在msdn網站上發現了現成的mock實現代碼(詳見 Testing with a mocking framework ),照此就可以輕松mock。
mock之前需要實現這三個接口:IDbAsyncEnumerator
1)TestDbAsyncEnumerator
public class TestDbAsyncEnumerator<T> : IDbAsyncEnumerator<T>
{
private readonly IEnumerator<T> _inner;
public TestDbAsyncEnumerator(IEnumerator<T> inner)
{
_inner = inner;
}
public void Dispose()
{
_inner.Dispose();
}
public Task<bool> MoveNextAsync(CancellationToken cancellationToken)
{
return Task.FromResult(_inner.MoveNext());
}
public T Current
{
get { return _inner.Current; }
}
object IDbAsyncEnumerator.Current
{
get { return Current; }
}
}
2)TestDbAsyncEnumerable
public class TestDbAsyncEnumerable<T> : EnumerableQuery<T>, IDbAsyncEnumerable<T>, IQueryable<T>
{
public TestDbAsyncEnumerable(IEnumerable<T> enumerable)
: base(enumerable)
{ }
public TestDbAsyncEnumerable(Expression expression)
: base(expression)
{ }
public IDbAsyncEnumerator<T> GetAsyncEnumerator()
{
return new TestDbAsyncEnumerator<T>(this.AsEnumerable().GetEnumerator());
}
IDbAsyncEnumerator IDbAsyncEnumerable.GetAsyncEnumerator()
{
return GetAsyncEnumerator();
}
IQueryProvider IQueryable.Provider
{
get { return new TestDbAsyncQueryProvider<T>(this); }
}
}
3)TestDbAsyncQueryProvider
public class TestDbAsyncQueryProvider<TEntity> : IDbAsyncQueryProvider
{
private readonly IQueryProvider _inner;
public TestDbAsyncQueryProvider(IQueryProvider inner)
{
_inner = inner;
}
public IQueryable CreateQuery(Expression expression)
{
return new TestDbAsyncEnumerable<TEntity>(expression);
}
public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
{
return new TestDbAsyncEnumerable<TElement>(expression);
}
public object Execute(Expression expression)
{
return _inner.Execute(expression);
}
public TResult Execute<TResult>(Expression expression)
{
return _inner.Execute<TResult>(expression);
}
public Task<object> ExecuteAsync(Expression expression, CancellationToken cancellationToken)
{
return Task.FromResult(Execute(expression));
}
public Task<TResult> ExecuteAsync<TResult>(Expression expression, CancellationToken cancellationToken)
{
return Task.FromResult(Execute<TResult>(expression));
}
}
然后將之前的mock代碼:
var mockUnitOfWork = new Mock<IUnitOfWork>();
mockUnitOfWork.Setup(u => u.Set<BlogCategory>()).Returns(blogCategories);
改為下面的代碼:
#region mockSet
var mockSet = new Mock<DbSet<BlogCategory>>();
mockSet.As<IDbAsyncEnumerable<BlogCategory>>()
.Setup(m => m.GetAsyncEnumerator())
.Returns(new TestDbAsyncEnumerator<BlogCategory>(blogCategories.GetEnumerator()));
mockSet.As<IQueryable<BlogCategory>>()
.Setup(m => m.Provider)
.Returns(new TestDbAsyncQueryProvider<BlogCategory>(blogCategories.Provider));
mockSet.As<IQueryable<BlogCategory>>().Setup(m => m.Expression).Returns(blogCategories.Expression);
mockSet.As<IQueryable<BlogCategory>>().Setup(m => m.ElementType).Returns(blogCategories.ElementType);
mockSet.As<IQueryable<BlogCategory>>().Setup(m => m.GetEnumerator()).Returns(blogCategories.GetEnumerator());
#endregion
var mockUnitOfWork = new Mock<IUnitOfWork>();
mockUnitOfWork.Setup(u => u.Set<BlogCategory>()).Returns(mockSet.Object);
這樣成功mock出DbSet
1 passed, 0 failed, 0 skipped, took 2.75 seconds (xUnit.net 1.9.2 build 1705).