使用xunit、Shouldy、NSubstitute 測試Entity Framework (EF6.0)

前言

此篇文章只針對ef6.0的特性和API,較早的版本,可能有部分或全部信息不適用。

參考

Entity Framework Testing with a Mocking Framework

NSubstitute完全手冊索引

源代碼

模擬對象的創建

有兩種不同的方法可以用來創建一個內存中的上下文

1.創建自定義模擬對象

創建自定義模擬對象,需要編寫自己的上下文和DbSets內存中的實現。
這樣我們就可以控制很對的類行為以及編寫合理數量的代碼。

2.適用Mocking框架創建模擬對象

使用模擬框架(如NSubstitute)你可以有內存的實現上下文和集在運行時動態創建

這篇文章主要主要是講述如何使用模擬框架。關于如何創建自定義模擬對象,可參閱:Testing with Your Own Test Doubles (EF6 onwards)

為了使用 EF 的 mocking 框架 , 我們將使用 NSubstitute。

測試更早版本

本文中顯示的方案取決于我們對EF6中的DbSet所做的一些更改。
對于使用EF5和更早版本的測試,請參閱使用Fake Context進行測試。

局限性

內存模擬測試是提供使用EF的應用程序單元測試級別覆蓋率的好方法。但是,在執行操作時,您正在使用的Linq to Obejcts來對內存中的數據執行查詢。這可能導致與使用EF的Linq提供程序(linq to entities )將查詢轉換為針對數據庫運行的sql 不同的行為。
比如,在加載相關數據方面。如果您創建了一系列具有相關帖子的博客,那么在使用內存中的數據時,相關帖子將始終被每個博客加載。但是,當對數據庫運行時,只有使用Include方法時,才會加載數據。
因此,建議始終包括一些級別的端對端測試(除了單元測試),以確保應用程序對數據庫正常工作。


使用準備

本文提供了完整的代碼清單,您可以將其復制到Visual Stuio中使用。
需要使用 .Net Framework4.5
1.創建單元測試項目
2.使用nuget添加一下包

Nuget.png

EF模型

我們要測試的服務使用由BloggingContext和Blog和Post類組成的EF模型。 此代碼可能是由EF Designer生成的,或者是Code First模型。

using System.Collections.Generic;
using System.Data.Entity;

namespace TestingDemo
{
    public class BloggingContext : DbContext
    {
        public virtual DbSet<Blog> Blogs { get; set; }
        public virtual DbSet<Post> Posts { get; set; }
    }

    public class Blog 
    { 
        public int BlogId { get; set; } 
        public string Name { get; set; } 
        public string Url { get; set; } 
 
        public virtual List<Post> Posts { get; set; } 
    } 
 
    public class Post 
    { 
        public int PostId { get; set; } 
        public string Title { get; set; } 
        public string Content { get; set; } 
 
        public int BlogId { get; set; } 
        public virtual Blog Blog { get; set; } 
    } 
}

在上下文 使用 virtual 標識 DbSet屬性

上下文中的DbSet 屬性標記為虛擬,這將允許模擬框架從我們自己的上下中導出,并使用模擬實現來覆蓋這些屬性。


測試服務

為了展示使用內存測試模擬對象,我們將為BlogService 編寫幾個測試。該服務能工創建新的博客(AddBlog)并返回所有按名次(GetAllBlogs)排序的博客。
除了GetAllBlogs之外,我們還提供了一種方法,它將異步地獲取按名稱(GetAllBlogsAsync)排序的所有博客。


using System.Collections.Generic; 
using System.Data.Entity; 
using System.Linq; 
using System.Threading.Tasks; 
 
namespace TestingDemo 
{ 
    public class BlogService 
    { 
        private BloggingContext _context; 
 
        public BlogService(BloggingContext context) 
        { 
            _context = context; 
        } 
 
        public Blog AddBlog(string name, string url) 
        { 
            var blog = _context.Blogs.Add(new Blog { Name = name, Url = url }); 
            _context.SaveChanges(); 
 
            return blog; 
        } 
 
        public List<Blog> GetAllBlogs() 
        { 
            var query = from b in _context.Blogs 
                        orderby b.Name 
                        select b; 
 
            return query.ToList(); 
        } 
 
        public async Task<List<Blog>> GetAllBlogsAsync() 
        { 
            var query = from b in _context.Blogs 
                        orderby b.Name 
                        select b; 
 
            return await query.ToListAsync(); 
        } 
    } 
}



測試非查詢

測試非查詢方案測試非查詢方案

這就是我們需要做的,以開始測試非查詢方法。 以下測試使用Moq創建上下文。 然后創建一個DbSet <Blog>并將其連接起來,從上下文的“博客”屬性返回。 接下來,上下文用于創建一個新的BlogService,然后用它來創建一個新的博客 - 使用AddBlog方法。 最后,測試驗證該服務在上下文中添加了一個新的Blog并調用SaveChanges。


using Microsoft.VisualStudio.TestTools.UnitTesting; 
using Moq; 
using System.Data.Entity; 
 
namespace TestingDemo 
{ 
    public class NonQueryTests
    {
        [Fact]
        public void CreateBlog_saves_a_blog_via_context()
        {
            var mockSet = Substitute.For<DbSet<Blog>>();
            var mockContext = Substitute.For<BloggingContext>();
            mockContext.Blogs.Returns(mockSet);

            var addBolg = new Blog(); // 用來驗證add 方法 傳入的參數使用為預期內容
            mockSet.Add(Arg.Do<Blog>(x => addBolg = x)); 

            var service = new BlogService(mockContext);
            service.AddBlog("ADO.NET Blog", "http://blogs.msdn.com/adonet");
            
            mockSet.Received(1).Add(Arg.Any<Blog>()); // set add 方法調用一次
            mockContext.Received(1).SaveChanges(); // context savechanges 方法調用一次
            
            // 實體屬性驗證
            addBolg.Name.ShouldBe("ADO.NET Blog");
            addBolg.Url.ShouldBe("http://blogs.msdn.com/adonet");

            addBolg.Name.ShouldNotBe("ADO.NET");
        }
    }
}

測試查詢

為了能夠針對我們的DbSet測試執行查詢,我們需要設置一個IQueryable的實現。 第一步是創建一些內存中的數據 - 我們使用的是List <Blog>。 接下來,我們創建一個上下文和DBSet <Blog>,然后連接DbSet的IQueryable實現 - 它們只是委托給與List <T>一起使用的LINQ to Objects提供程序。為了能夠針對我們的DbSet測試執行查詢,我們需要設置一個IQueryable的實現。 第一步是創建一些內存中的數據 - 我們使用的是List <Blog>。 接下來,我們創建一個上下文和DBSet <Blog>,然后連接DbSet的IQueryable實現 - 它們只是委托給與List <T>一起使用的LINQ to Objects提供程序。


using Microsoft.VisualStudio.TestTools.UnitTesting; 
using Moq; 
using System.Collections.Generic; 
using System.Data.Entity; 
using System.Linq; 
 
namespace TestingDemo 
{ 
    public class QueryTests
    {
        [Fact]
        public void GetAllBlogs_orders_by_name()
        {
            var data = new List<Blog>
                           {
                               new Blog { Name = "BBB" },
                               new Blog { Name = "ZZZ" },
                               new Blog { Name = "AAA" },
                           }.AsQueryable();

            var mockSet = Substitute.For<DbSet<Blog>, IQueryable<Blog>>();
            ((IQueryable<Blog>)mockSet).Provider.Returns(data.Provider);
            ((IQueryable<Blog>)mockSet).Expression.Returns(data.Expression);
            ((IQueryable<Blog>)mockSet).ElementType.Returns(data.ElementType);
            ((IQueryable<Blog>)mockSet).GetEnumerator().Returns(data.GetEnumerator());
            
            var mockContext = Substitute.For<BloggingContext>();
            mockContext.Blogs.Returns(mockSet);
            var service = new BlogService(mockContext);
            var blogs = service.GetAllBlogs();
            blogs.Count.ShouldBe(3);
            blogs[0].Name.ShouldBe("AAA");
            blogs[1].Name.ShouldBe("BBB");
            blogs[2].Name.ShouldBe("ZZZ");
            
        }
    }
}


測試異步查詢方案

為了使用異步查詢,我們需要做更多的工作。 如果我們嘗試使用我們的Moq DbSet與GetAllBlogsAsync方法,我們將得到以下異常:為了使用異步查詢,我們需要做更多的工作。 如果我們嘗試使用我們的Moq DbSet與GetAllBlogsAsync方法,我們將得到以下異常:

System.InvalidOperationException: The source IQueryable doesn't implement IDbAsyncEnumerable<TestingDemo.Blog>. Only sources that implement IDbAsyncEnumerable can be used for Entity Framework asynchronous operations. For more details see http://go.microsoft.com/fwlink/?LinkId=287068._System.InvalidOperationException

為了使用異步方法,我們需要創建一個內存中的DbAsyncQueryProvider來處理異步查詢。 雖然可以使用Moq設置查詢提供程序,但是在代碼中創建測試雙重實現要容易得多。 此實現的代碼如下:


using System.Collections.Generic; 
using System.Data.Entity.Infrastructure; 
using System.Linq; 
using System.Linq.Expressions; 
using System.Threading; 
using System.Threading.Tasks; 
 
namespace TestingDemo 
{ 
    internal class TestDbAsyncQueryProvider<TEntity> : IDbAsyncQueryProvider 
    { 
        private readonly IQueryProvider _inner; 
 
        internal 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)); 
        } 
    } 
 
    internal 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); } 
        } 
    } 
 
    internal 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; } 
        } 
    } 
}


現在我們有一個異步查詢提供程序,我們可以為我們的新的GetAllBlogsAsync方法編寫單元測試。現在我們有一個異步查詢提供程序,我們可以為我們的新的GetAllBlogsAsync方法編寫單元測試。


using Microsoft.VisualStudio.TestTools.UnitTesting; 
using Moq; 
using System.Collections.Generic; 
using System.Data.Entity; 
using System.Data.Entity.Infrastructure; 
using System.Linq; 
using System.Threading.Tasks; 
 
namespace TestingDemo 
{ 
    public class AsyncQueryTests
    {
        [Fact]
        public async Task GetAllBlogsAsync_orders_by_name()
        {
            var data = new List<Blog>
                           {
                               new Blog { Name = "BBB" },
                               new Blog { Name = "ZZZ" },
                               new Blog { Name = "AAA" },
                           }.AsQueryable();

            var mockSet = Substitute.For<DbSet<Blog>, IQueryable<Blog>, IDbAsyncEnumerable<Blog>>();

            ((IDbAsyncEnumerable<Blog>)mockSet).GetAsyncEnumerator()
                .Returns(new TestDbAsyncEnumerator<Blog>(data.GetEnumerator()));
            ((IQueryable<Blog>)mockSet).Provider
                .Returns(new TestDbAsyncQueryProvider<Blog>(data.Provider));

            ((IQueryable<Blog>)mockSet).Expression
                .Returns(data.Expression);
            ((IQueryable<Blog>)mockSet).ElementType
                .Returns(data.ElementType);
            ((IQueryable<Blog>)mockSet).GetEnumerator()
                .Returns(data.GetEnumerator());
            
            var mockContext = Substitute.For<BloggingContext>();
            mockContext.Blogs.Returns(mockSet);
            var service = new BlogService(mockContext);
            var blogs = await service.GetAllBlogsAsync();
            blogs.Count.ShouldBe(3);
            blogs[0].Name.ShouldBe("AAA");
            blogs[1].Name.ShouldBe("BBB");
            blogs[2].Name.ShouldBe("ZZZ");
        }
    }
}



QQ:1260825783(諸葛小亮)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,933評論 18 139
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,333評論 25 708
  • 打開Dump文件調試 VS方式:用VS打開dumpVS調試a 根據截圖中“1”的位置確定程序安裝路徑,將同版本的程...
    龍翱天際閱讀 4,643評論 0 0
  • “如何度過生命的最后24小時?”要被問起這個問題,你會怎么回答? 如果你說,24小時太短暫了,想做的事情實在太多了...
    言射手閱讀 1,165評論 3 10
  • 聽完了《戴紅袖標的大象》,我請孩子們畫出印象最深刻的或者說最喜歡的一個角色、場景。 他先是用棕...
    xiaotu_ruo閱讀 357評論 0 0