共用方式為


使用您自己的測試替身進行測試

注意

僅限 EF6 及更新版本 - Entity Framework 6 已引進此頁面中所討論的功能及 API 等等。 如果您使用的是較早版本,則不適用部分或全部的資訊。

撰寫應用程式的測試時,通常最好避免達到資料庫。 Entity Framework 可讓您藉由建立內容來達成此目的,其中包含測試所定義的行為,以利用記憶體內部數據。

建立測試雙精度浮點數的選項

有兩種不同的方法可用來建立內容記憶體內部版本。

  • 建立您自己的測試雙精度浮點 數 – 此方法牽涉到撰寫內容和 DbSets 的記憶體內部實作。 這可讓您充分掌控類別的行為,但可能牽涉到撰寫及擁有合理的程式代碼數量。
  • 使用模擬架構來建立測試雙精度浮 點數 – 使用模擬架構(例如 Moq),您可以擁有內容的記憶體記憶體記憶體內實作,並在運行時間動態為您建立集合。

本文將處理建立您自己的測試雙精度浮點數。 如需使用模擬架構的資訊,請參閱 使用模擬架構進行測試。

使用EF6前版本進行測試

本文中顯示的程序代碼與 EF6 相容。 若要使用 EF5 和舊版進行測試,請參閱 使用 Fake Context 進行測試。

EF 記憶體內部測試雙精度浮點數的限制

記憶體內部測試雙精度浮點數是提供使用 EF 之應用程式的單元測試層級涵蓋範圍的好方法。 不過,當您這樣做時,您會使用 LINQ to Objects 對記憶體內部資料執行查詢。 這可能會導致與使用 EF 的 LINQ 提供者 (LINQ to Entities) 不同的行為,將查詢轉譯成針對資料庫執行的 SQL。

這類差異的其中一個範例是載入相關數據。 如果您建立一系列具有相關文章的部落格,則在使用記憶體內部數據時,每個部落格一律會載入相關的文章。 不過,針對資料庫執行時,只有在您使用 Include 方法時,才會載入數據。

基於這個理由,建議一律包含一些端對端測試層級(除了單元測試外),以確保您的應用程式可針對資料庫正確運作。

遵循這篇文章

本文提供完整的程式代碼清單,您可以視需要複製到 Visual Studio 中以遵循。 建立單元測試專案最簡單的方式,您必須以 .NET Framework 4.5 為目標,才能完成使用異步的區段。

建立內容介面

我們將探討如何測試使用 EF 模型的服務。 為了能夠將 EF 內容取代為記憶體內部版本進行測試,我們將定義 EF 內容(且其記憶體內部雙精度浮點數)將實作的介面。

我們將測試的服務會使用內容中的 DbSet 屬性來查詢和修改數據,並呼叫 SaveChanges 將變更推送至資料庫。 因此,我們會在介面中包含這些成員。

using System.Data.Entity;

namespace TestingDemo
{
    public interface IBloggingContext
    {
        DbSet<Blog> Blogs { get; }
        DbSet<Post> Posts { get; }
        int SaveChanges();
    }
}

EF 模型

我們將測試的服務會使用由 BloggingContext 和 Blog 和 Post 類別組成的 EF 模型。 此程式代碼可能是由EF設計工具產生,或是程式代碼優先模型。

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

namespace TestingDemo
{
    public class BloggingContext : DbContext, IBloggingContext
    {
        public DbSet<Blog> Blogs { get; set; }
        public 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; }
    }
}

使用EF設計工具實作內容介面

請注意,我們的內容會實作 IBloggingContext 介面。

如果您使用 Code First,則可以直接編輯內容來實作 介面。 如果您使用 EF 設計工具,則必須編輯產生內容的 T4 範本。 <開啟您edmx檔案下巢狀的> model_name.Context.tt 檔案,尋找下列程式代碼片段,並在介面中新增,如下所示。

<#=Accessibility.ForType(container)#> partial class <#=code.Escape(container)#> : DbContext, IBloggingContext

要測試的服務

為了示範使用記憶體內部測試的雙重測試,我們將撰寫一些 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 IBloggingContext _context;

        public BlogService(IBloggingContext context)
        {
            _context = context;
        }

        public Blog AddBlog(string name, string url)
        {
            var blog = new Blog { Name = name, Url = url };
            _context.Blogs.Add(blog);
            _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();
        }
    }
}

建立記憶體內部測試雙精度浮點數

既然我們有實際的 EF 模型和服務可以使用它,現在可以建立可用於測試的記憶體內部測試雙精度浮點數。 我們已為內容建立TestContext測試雙精度浮點數。 在測試雙精度浮點數中,我們會選擇我們想要的行為,以支援我們要執行的測試。 在此範例中,我們只是擷取呼叫 SaveChanges 的次數,但您可以包含驗證您要測試之案例所需的任何邏輯。

我們也建立了 TestDbSet,以提供 DbSet 的記憶體內部實作。 我們已提供 DbSet 上所有方法的完整實作(除了 Find 除外),但您只需要實作測試案例將使用的成員。

TestDbSet 會使用我們所包含的一些其他基礎結構類別,以確保可以處理異步查詢。

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;

namespace TestingDemo
{
    public class TestContext : IBloggingContext
    {
        public TestContext()
        {
            this.Blogs = new TestDbSet<Blog>();
            this.Posts = new TestDbSet<Post>();
        }

        public DbSet<Blog> Blogs { get; set; }
        public DbSet<Post> Posts { get; set; }
        public int SaveChangesCount { get; private set; }
        public int SaveChanges()
        {
            this.SaveChangesCount++;
            return 1;
        }
    }

    public class TestDbSet<TEntity> : DbSet<TEntity>, IQueryable, IEnumerable<TEntity>, IDbAsyncEnumerable<TEntity>
        where TEntity : class
    {
        ObservableCollection<TEntity> _data;
        IQueryable _query;

        public TestDbSet()
        {
            _data = new ObservableCollection<TEntity>();
            _query = _data.AsQueryable();
        }

        public override TEntity Add(TEntity item)
        {
            _data.Add(item);
            return item;
        }

        public override TEntity Remove(TEntity item)
        {
            _data.Remove(item);
            return item;
        }

        public override TEntity Attach(TEntity item)
        {
            _data.Add(item);
            return item;
        }

        public override TEntity Create()
        {
            return Activator.CreateInstance<TEntity>();
        }

        public override TDerivedEntity Create<TDerivedEntity>()
        {
            return Activator.CreateInstance<TDerivedEntity>();
        }

        public override ObservableCollection<TEntity> Local
        {
            get { return _data; }
        }

        Type IQueryable.ElementType
        {
            get { return _query.ElementType; }
        }

        Expression IQueryable.Expression
        {
            get { return _query.Expression; }
        }

        IQueryProvider IQueryable.Provider
        {
            get { return new TestDbAsyncQueryProvider<TEntity>(_query.Provider); }
        }

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return _data.GetEnumerator();
        }

        IEnumerator<TEntity> IEnumerable<TEntity>.GetEnumerator()
        {
            return _data.GetEnumerator();
        }

        IDbAsyncEnumerator<TEntity> IDbAsyncEnumerable<TEntity>.GetAsyncEnumerator()
        {
            return new TestDbAsyncEnumerator<TEntity>(_data.GetEnumerator());
        }
    }

    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; }
        }
    }
}

實作尋找

Find 方法難以以泛型方式實作。 如果您需要測試使用 Find 方法的程式碼,最簡單的方式是針對需要支持尋找的每個實體類型建立測試 DbSet。 接著,您可以撰寫邏輯來尋找該特定類型的實體,如下所示。

using System.Linq;

namespace TestingDemo
{
    class TestBlogDbSet : TestDbSet<Blog>
    {
        public override Blog Find(params object[] keyValues)
        {
            var id = (int)keyValues.Single();
            return this.SingleOrDefault(b => b.BlogId == id);
        }
    }
}

撰寫一些測試

這就是開始測試所需的一切。 下列測試會建立 TestContext,然後根據此內容建立服務。 接著會使用服務來建立新的部落格 – 使用 AddBlog 方法。 最後,測試會確認服務已將新的 Blog 新增至內容的 Blogs 屬性,並在內容上呼叫 SaveChanges。

這隻是您可以使用記憶體內測試雙精度浮點數測試的類型範例,而且您可以調整測試雙精度浮點數和驗證的邏輯,以符合您的需求。

using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Linq;

namespace TestingDemo
{
    [TestClass]
    public class NonQueryTests
    {
        [TestMethod]
        public void CreateBlog_saves_a_blog_via_context()
        {
            var context = new TestContext();

            var service = new BlogService(context);
            service.AddBlog("ADO.NET Blog", "http://blogs.msdn.com/adonet");

            Assert.AreEqual(1, context.Blogs.Count());
            Assert.AreEqual("ADO.NET Blog", context.Blogs.Single().Name);
            Assert.AreEqual("http://blogs.msdn.com/adonet", context.Blogs.Single().Url);
            Assert.AreEqual(1, context.SaveChangesCount);
        }
    }
}

以下是另一個測試範例 - 這次是執行查詢的範例。 測試一開始會建立含有其 Blog 屬性中某些資料的測試內容 , 請注意資料不是依字母順序排列。 然後,我們可以根據測試內容建立 BlogService,並確定我們從 GetAllBlogs 取得的數據會依名稱排序。

using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace TestingDemo
{
    [TestClass]
    public class QueryTests
    {
        [TestMethod]
        public void GetAllBlogs_orders_by_name()
        {
            var context = new TestContext();
            context.Blogs.Add(new Blog { Name = "BBB" });
            context.Blogs.Add(new Blog { Name = "ZZZ" });
            context.Blogs.Add(new Blog { Name = "AAA" });

            var service = new BlogService(context);
            var blogs = service.GetAllBlogs();

            Assert.AreEqual(3, blogs.Count);
            Assert.AreEqual("AAA", blogs[0].Name);
            Assert.AreEqual("BBB", blogs[1].Name);
            Assert.AreEqual("ZZZ", blogs[2].Name);
        }
    }
}

最後,我們將再撰寫一個使用異步方法的測試,以確保 TestDbSet 中包含的異步基礎結構正常運作。

using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace TestingDemo
{
    [TestClass]
    public class AsyncQueryTests
    {
        [TestMethod]
        public async Task GetAllBlogsAsync_orders_by_name()
        {
            var context = new TestContext();
            context.Blogs.Add(new Blog { Name = "BBB" });
            context.Blogs.Add(new Blog { Name = "ZZZ" });
            context.Blogs.Add(new Blog { Name = "AAA" });

            var service = new BlogService(context);
            var blogs = await service.GetAllBlogsAsync();

            Assert.AreEqual(3, blogs.Count);
            Assert.AreEqual("AAA", blogs[0].Name);
            Assert.AreEqual("BBB", blogs[1].Name);
            Assert.AreEqual("ZZZ", blogs[2].Name);
        }
    }
}