使用您自己的測試替身進行測試
注意
僅限 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);
}
}
}