• 单元测试中使用Moq对EF的DbSet进行mock


    刚用上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转换过来的IQueryable并没有实现这个接口。要解决这个问题,我们需要使用实现IDbAsyncEnumerable相关接口的集合类型,而EF中已经天然内置了这样的集合类型,它就是DbSet。只要能mock出DbSet,问题就迎刃而解。

    解决问题

    那如何mock呢?比想象中复杂得多,幸好在msdn网站上发现了现成的mock实现代码(详见 Testing with a mocking framework ),照此就可以轻松mock。
    mock之前需要实现这三个接口:IDbAsyncEnumerator,IDbAsyncEnumerable,IDbAsyncQueryProvider 。
    1)TestDbAsyncEnumerator 实现 IDbAsyncEnumerator

    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

    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

    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).
    
  • 相关阅读:
    WinForm:实现类似QQ消息框一样的右下角消息提示窗口
    WinForm:系统托盘NotifyIcon
    多线程学习系列:(一)前言
    Codeforces Round #176 (Div. 2)总结A. IQ Test
    使用STL的next_permutation函数生成全排列(C++)
    c语言字符串 数字转换函数大全
    c语言中字符串处理函数
    杭电OJ题目分类
    Codeforces Beta Round #77 (Div. 2 Only)A. Football
    算法导论第三版目录
  • 原文地址:https://www.cnblogs.com/dudu/p/unittest-moq-mock-ef-dbset.html
Copyright © 2020-2023  润新知