最近在作一個項目的單元測試時,遇到了些問題,解決後,以爲有必要記下來,並分享給須要的人,先簡單說一下項目技術框架背景:git
在對業務層進行單元測試時,由於業務層調用到數據處理層,因此要用Moq去模擬DbContext,這個很容易作到,但若是操做DbContext下的DbSet和DbSet下的擴展方法時,就會拋出一個System.NotSupportedException異常。這是由於咱們沒辦法Mock DbSet,並助DbSet是個抽象類,尚未辦法實例化。github
其實,這個時候咱們但願的是,若是用一個通用的集合,好比List<T>集合,或T[]數組來Mock DbSet<T>,就很是舒服了,由於集合或數組的元素咱們很是容易模擬或控制,不像DbSet。express
深挖DbSet下經常使用的這些擴展方法:Where,Select,SingleOrDefault,FirstOrDefault,OrderBy等,都是對IQueryable的擴展,也就是說把對DbSet的這些擴展方法的調用轉成Mock List<T>或T[]的擴展方法調用就OK了,數組
因此實現下的類型:框架
項目須要引入:Microsoft.EntityFrameworkCore 和Moq,Nuget能夠引入。asp.net
UnitTestAsyncEnumerable.cside
1 using System.Collections.Generic; 2 using System.Linq; 3 using System.Linq.Expressions; 4 5 namespace MoqEFCoreExtension 6 { 7 /// <summary> 8 /// 自定義實現EnumerableQuery<T>, IAsyncEnumerable<T>, IQueryable<T>類型 9 /// </summary> 10 /// <typeparam name="T"></typeparam> 11 class UnitTestAsyncEnumerable<T> : EnumerableQuery<T>, IAsyncEnumerable<T>, IQueryable<T> 12 { 13 public UnitTestAsyncEnumerable(IEnumerable<T> enumerable) 14 : base(enumerable) 15 { } 16 17 public UnitTestAsyncEnumerable(Expression expression) 18 : base(expression) 19 { } 20 21 public IAsyncEnumerator<T> GetEnumerator() 22 { 23 return new UnitTestAsyncEnumerator<T>(this.AsEnumerable().GetEnumerator()); 24 } 25 26 IQueryProvider IQueryable.Provider 27 { 28 get { return new UnitTestAsyncQueryProvider<T>(this); } 29 } 30 } 31 }
UnitTestAsyncEnumerator.cs單元測試
1 using System.Collections.Generic; 2 using System.Threading; 3 using System.Threading.Tasks; 4 5 namespace MoqEFCoreExtension 6 { 7 /// <summary> 8 /// 定義關現IAsyncEnumerator<T>類型 9 /// </summary> 10 /// <typeparam name="T"></typeparam> 11 class UnitTestAsyncEnumerator<T> : IAsyncEnumerator<T> 12 { 13 private readonly IEnumerator<T> _inner; 14 15 public UnitTestAsyncEnumerator(IEnumerator<T> inner) 16 { 17 _inner = inner; 18 } 19 20 public void Dispose() 21 { 22 _inner.Dispose(); 23 } 24 25 public T Current 26 { 27 get 28 { 29 return _inner.Current; 30 } 31 } 32 33 public Task<bool> MoveNext(CancellationToken cancellationToken) 34 { 35 return Task.FromResult(_inner.MoveNext()); 36 } 37 } 38 }
UnitTestAsyncQueryProvider.cs測試
1 using Microsoft.EntityFrameworkCore.Query.Internal; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Linq.Expressions; 5 using System.Threading; 6 using System.Threading.Tasks; 7 8 namespace MoqEFCoreExtension 9 { 10 /// <summary> 11 /// 實現IQueryProvider接口 12 /// </summary> 13 /// <typeparam name="TEntity"></typeparam> 14 class UnitTestAsyncQueryProvider<TEntity> : IAsyncQueryProvider 15 { 16 private readonly IQueryProvider _inner; 17 18 internal UnitTestAsyncQueryProvider(IQueryProvider inner) 19 { 20 _inner = inner; 21 } 22 23 public IQueryable CreateQuery(Expression expression) 24 { 25 return new UnitTestAsyncEnumerable<TEntity>(expression); 26 } 27 28 public IQueryable<TElement> CreateQuery<TElement>(Expression expression) 29 { 30 return new UnitTestAsyncEnumerable<TElement>(expression); 31 } 32 33 public object Execute(Expression expression) 34 { 35 return _inner.Execute(expression); 36 } 37 38 public TResult Execute<TResult>(Expression expression) 39 { 40 return _inner.Execute<TResult>(expression); 41 } 42 43 public IAsyncEnumerable<TResult> ExecuteAsync<TResult>(Expression expression) 44 { 45 return new UnitTestAsyncEnumerable<TResult>(expression); 46 } 47 48 public Task<TResult> ExecuteAsync<TResult>(Expression expression, CancellationToken cancellationToken) 49 { 50 return Task.FromResult(Execute<TResult>(expression)); 51 } 52 } 53 }
擴展方法類EFSetupData.csthis
1 using Microsoft.EntityFrameworkCore; 2 using Moq; 3 using System.Collections.Generic; 4 using System.Linq; 5 6 7 namespace MoqEFCoreExtension 8 { 9 /// <summary> 10 /// Mock Entity Framework Core中DbContext,加載List<T>或T[]到DbSet<T> 11 /// </summary> 12 public static class EFSetupData 13 { 14 /// <summary> 15 /// 加載List<T>到DbSet 16 /// </summary> 17 /// <typeparam name="T">實體類型</typeparam> 18 /// <param name="mockSet">Mock<DbSet>對象</param> 19 /// <param name="list">實體列表</param> 20 /// <returns></returns> 21 public static Mock<DbSet<T>> SetupList<T>(this Mock<DbSet<T>> mockSet, List<T> list) where T : class 22 { 23 return mockSet.SetupArray(list.ToArray()); 24 } 25 /// <summary> 26 /// 加載數據到DbSet 27 /// </summary> 28 /// <typeparam name="T">實體類型</typeparam> 29 /// <param name="mockSet">Mock<DbSet>對象</param> 30 /// <param name="array">實體數組</param> 31 /// <returns></returns> 32 public static Mock<DbSet<T>> SetupArray<T>(this Mock<DbSet<T>> mockSet, params T[] array) where T : class 33 { 34 var queryable = array.AsQueryable(); 35 mockSet.As<IAsyncEnumerable<T>>().Setup(m => m.GetEnumerator()).Returns(new UnitTestAsyncEnumerator<T>(queryable.GetEnumerator())); 36 mockSet.As<IQueryable<T>>().Setup(m => m.Provider).Returns(new UnitTestAsyncQueryProvider<T>(queryable.Provider)); 37 mockSet.As<IQueryable<T>>().Setup(m => m.Expression).Returns(queryable.Expression); 38 mockSet.As<IQueryable<T>>().Setup(m => m.ElementType).Returns(queryable.ElementType); 39 mockSet.As<IQueryable<T>>().Setup(m => m.GetEnumerator()).Returns(() => queryable.GetEnumerator()); 40 return mockSet; 41 } 42 } 43 }
var answerSet = new Mock<DbSet<Answers>>().SetupList(list);替換擴展方法,以致於在answerRepository.ModifyAnswer(answer)中調用SingleOrDefault時,操做的是具備兩個answers的list,而非DbSet。
源碼和Sample:https://github.com/axzxs2001/MoqEFCoreExtension
同時,我把這個功能封閉成了一個Nuget包,參見:https://www.nuget.org/packages/MoqEFCoreExtension/
最後上一個圖壓壓驚: