在XUnit中用Moq怎樣模擬EntityFramework Core下的DbSet

最近在作一個項目的單元測試時,遇到了些問題,解決後,以爲有必要記下來,並分享給須要的人,先簡單說一下項目技術框架背景:git

  • asp.net core 2.0(for .net core)框架
  • 用Entity Framework Core做ORM
  • XUnit做單元測試
  • Moq做隔離框加

 在對業務層進行單元測試時,由於業務層調用到數據處理層,因此要用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/

最後上一個圖壓壓驚:

相關文章
相關標籤/搜索