最近在做一个项目的单元测试时,遇到了些问题,解决后,觉得有必要记下来,并分享给需要的人,先简单说一下项目技术框架背景:
- asp.net core 2.0(for .net core)框架
- 用Entity Framework Core作ORM
- XUnit作单元测试
- Moq作隔离框加
在对业务层进行单元测试时,因为业务层调用到数据处理层,所以要用Moq去模拟DbContext,这个很容易做到,但如果操作DbContext下的DbSet和DbSet下的扩展方法时,就会抛出一个System.NotSupportedException异常。这是因为我们没办法Mock DbSet,并助DbSet是个抽象类,还没有办法实例化。
其实,这个时候我们希望的是,如果用一个通用的集合,比如List
深挖DbSet下常用的这些扩展方法:Where,Select,SingleOrDefault,FirstOrDefault,OrderBy等,都是对IQueryable的扩展,也就是说把对DbSet的这些扩展方法的调用转成Mock List
所以实现下的类型:
项目需要引入:Microsoft.EntityFrameworkCore 和Moq,Nuget可以引入。
UnitTestAsyncEnumerable.cs
1 using System.Collections.Generic; 2 using System.Linq; 3 using System.Linq.Expressions; 4 5 namespace MoqEFCoreExtension 6 { 7 ///8 /// 自定义实现EnumerableQuery , IAsyncEnumerable , IQueryable 类型 9 /// 10 /// 11 class UnitTestAsyncEnumerable : EnumerableQuery , IAsyncEnumerable , IQueryable 12 { 13 public UnitTestAsyncEnumerable(IEnumerable enumerable) 14 : base(enumerable) 15 { } 16 17 public UnitTestAsyncEnumerable(Expression expression) 18 : base(expression) 19 { } 20 21 public IAsyncEnumerator GetEnumerator() 22 { 23 return new UnitTestAsyncEnumerator (this.AsEnumerable().GetEnumerator()); 24 } 25 26 IQueryProvider IQueryable.Provider 27 { 28 get { return new UnitTestAsyncQueryProvider (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 ///8 /// 定义关现IAsyncEnumerator 类型 9 /// 10 /// 11 class UnitTestAsyncEnumerator : IAsyncEnumerator 12 { 13 private readonly IEnumerator _inner; 14 15 public UnitTestAsyncEnumerator(IEnumerator 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 ///11 /// 实现IQueryProvider接口 12 /// 13 /// 14 class UnitTestAsyncQueryProvider : 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 (expression); 26 } 27 28 public IQueryable CreateQuery (Expression expression) 29 { 30 return new UnitTestAsyncEnumerable (expression); 31 } 32 33 public object Execute(Expression expression) 34 { 35 return _inner.Execute(expression); 36 } 37 38 public TResult Execute (Expression expression) 39 { 40 return _inner.Execute (expression); 41 } 42 43 public IAsyncEnumerable ExecuteAsync (Expression expression) 44 { 45 return new UnitTestAsyncEnumerable (expression); 46 } 47 48 public Task ExecuteAsync (Expression expression, CancellationToken cancellationToken) 49 { 50 return Task.FromResult(Execute (expression)); 51 } 52 } 53 }
扩展方法类EFSetupData.cs
1 using Microsoft.EntityFrameworkCore; 2 using Moq; 3 using System.Collections.Generic; 4 using System.Linq; 5 6 7 namespace MoqEFCoreExtension 8 { 9 ///10 /// Mock Entity Framework Core中DbContext,加载List 或T[]到DbSet 11 /// 12 public static class EFSetupData 13 { 14 /// 15 /// 加载List 到DbSet 16 /// 17 /// 实体类型 18 /// Mock 对象 19 /// 实体列表 20 /// 21 public static Mock > SetupList (this Mock > mockSet, List list) where T : class 22 { 23 return mockSet.SetupArray(list.ToArray()); 24 } 25 /// 26 /// 加载数据到DbSet 27 /// 28 /// 实体类型 29 /// Mock 对象 30 /// 实体数组 31 /// 32 public static Mock > SetupArray (this Mock > mockSet, params T[] array) where T : class 33 { 34 var queryable = array.AsQueryable(); 35 mockSet.As >().Setup(m => m.GetEnumerator()).Returns(new UnitTestAsyncEnumerator (queryable.GetEnumerator())); 36 mockSet.As >().Setup(m => m.Provider).Returns(new UnitTestAsyncQueryProvider (queryable.Provider)); 37 mockSet.As >().Setup(m => m.Expression).Returns(queryable.Expression); 38 mockSet.As >().Setup(m => m.ElementType).Returns(queryable.ElementType); 39 mockSet.As >().Setup(m => m.GetEnumerator()).Returns(() => queryable.GetEnumerator()); 40 return mockSet; 41 } 42 } 43 }
var answerSet = new Mock
源码和Sample:https://github.com/axzxs2001/MoqEFCoreExtension
同时,我把这个功能封闭成了一个Nuget包,参见:https://www.nuget.org/packages/MoqEFCoreExtension/
最后上一个图压压惊: