刚用上Moq ,就用它解决了一个IUnitOfWork的mock问题,在这篇博文中记录一下。
开发场景 Application服务层BlogCategoryService的实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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的实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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集合,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 [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)); }
遇到问题 运行单元测试时,却出现下面的错误:
1 2 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转换过来的IQueryable并没有实现这个接口。要解决这个问题,我们需要使用实现IDbAsyncEnumerable相关接口的集合类型,而EF中已经天然内置了这样的集合类型,它就是DbSet。只要能mock出DbSet,问题就迎刃而解。
解决问题 那如何mock呢?比想象中复杂得多,幸好在msdn网站上发现了现成的mock实现代码(详见 Testing with a mocking framework ),照此就可以轻松mock。 mock之前需要实现这三个接口:IDbAsyncEnumerator,IDbAsyncEnumerable,IDbAsyncQueryProvider 。 1)TestDbAsyncEnumerator 实现 IDbAsyncEnumerator
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 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 实现 IDbAsyncEnumerable
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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 实现 IDbAsyncQueryProvider
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 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代码:
1 2 var mockUnitOfWork = new Mock<IUnitOfWork>(); mockUnitOfWork.Setup(u => u.Set<BlogCategory>()).Returns(blogCategories);
改为下面的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #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 1 passed, 0 failed, 0 skipped, took 2.75 seconds (xUnit.net 1.9.2 build 1705).