單元測試中使用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).


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM