帮酷LOGO
0 0 评论
  • 显示原文与译文双语对照的内容
文章标签:REPO  Repository  rep  


簡介

story

本文演示如何使用存儲庫 單元測試 代碼,或者直接使用 Entity Framework。

的故事

在工作中,我們使用了一些東西來做我們的datalayer代碼。 這包括原始的ADO. NET/Dapper 和 NHibernate。 我已經使用 NHibernate 大約 2年了,但在過去我使用了 Entity Framework。 許多它的他人一樣,我提出了允許我更容易地測試數據訪問代碼的模式。 這通常意味著使用存儲庫 Pattern,它作為對實際資料庫的抽象。 可以使用存儲庫 Pattern,它允許你創建模擬/測試替身,允許在不依賴資料庫的情況下測試所有的代碼。

repository存儲庫模式do我喜歡在一個單元中傳遞抽象,這樣許多存儲庫操作就可以在一個提交中組合在一起。 在 NHibernate 中,這將是ISession抽象。 使用 Entity Framework 時,我們並沒有使用通用介面抽象來表示一個單元,但是我們可以通過創建一個介面和 Entity Framework DbContext 來實現我們自己的介面實現。

無論如何,我們離題了,我的觀點是我通常使用存儲庫 Pattern 來使我的代碼更具可以測試性。 這是 Entity Framework 團隊在代碼中直接在代碼中使用 DbContext ( 這就是本文要使用的)的第一天,因為他們已經做了很多工作。 博客發布的是 Entity Framework 團隊,他們正在討論 Entity Framework的當前( 在編寫 EF 6時) 版本,這也是本文所基於的內容。

這不是我第一次看到博客文章告訴我們所有的存儲庫,事實上,Ayende是 NHibernate的主要驅動器和

http://ayende.com/blog/3955/repository-is-the-new-singleton
http://ayende.com/blog/4784/architecting-in-the-pit-of-doom-the-evils-of-the-repository-abstraction-layer

Mmmmm有趣。

正如我在工作中所說的,我使用 NHibernate ( 我也使用存儲庫來幫助測試,儘管我不擔心創建規範類,為什麼在 IQueryable 和exression樹上。),但是我對 Entity Framework 有一個軟點,所以本文使用。

所以,考慮到這一點,我決定創建 2個簡單的類來測試以下內容:

  • 第一類將在存儲庫上為它的所有數據訪問取一個 dependancy
  • 第二個類將直接使用 Entity Framework

對於這些場景,將有一些代碼,以及一些驗證代碼的測試。

致歉。

正如本文所介紹的一樣,本文文本中沒有很多可以說明的內容,就是測試系統。 這篇文章中有很多代碼,不是很多,因這裡我提前對這些代碼很有用。

代碼在哪裡

代碼正在我的github帳戶中自動切換: https://github.com/sachabarber/TestingEntityFramework

先決條件

要運行與本文關聯的代碼,你需要執行以下操作:

  • SQL Server 安裝
  • 針對新的( 或者現有) 資料庫運行以下 2個安裝腳本。
    • DB ScriptsCreate Posts.sql
    • DB ScriptsCreate Post Comments.sql
  • 確保已經更改下列項目中的App.Config 文件以反映你的SQL Server 安裝
    • EFTest.csproj
    • EFTest.WithRepositories.csproj
使用存儲庫的測試
  • 請參閱:EFTest.WithRepositories.csproj
  • 請參閱:EFTest.WithRepositories.Tests.csproj

本節討論了一組利用存儲庫 Pattern 服務的文件。,還有一個基於xml的工作單元的抽象。

在測試中使用存儲庫( ) 來測試系統。

這是我們將要測試的類:

publicclass SomeService : ISomeService, IDisposable
{
 privatereadonly IUnitOfWork context;
 privatereadonly IRepository<Post> repository;
 privateint counter;
 public SomeService(IUnitOfWork context, IRepository<Post> repository)
 {
 this.context = context;
 this.repository = repository;
 }
 publicvoid Insert(string url)
 {
 Post post = new Post() { Url = url };
 post.PostComments.Add(new PostComment()
 {
 Comment = string.Format("yada yada {0}", counter++)
 });
 repository.Add(post);
 }
 public IEnumerable<Post> GetAll()
 {
 return repository.GetAll();
 }
 public IEnumerable<Post> GetAll(Expression<Func<Post, bool>> filter)
 {
 return repository.GetAll(filter);
 }
 public Post FindById(int id)
 {
 var post = repository.Get(id);
 return post;
 }
 public Task<bool> InsertAsync(string url)
 {
 Post post = new Post() { Url = url };
 post.PostComments.Add(new PostComment()
 {
 Comment = string.Format("yada yada {0}", counter++)
 });
 return repository.AddAsync(post);
 }
 publicasync Task<List<Post>> GetAllAsync()
 {
 var posts = await repository.GetAllAsync();
 return posts.ToList();
 }
 public Task<Post> FindByIdAsync(int id)
 {
 return repository.GetIncludingAsync(id, x => x.PostComments);
 }
 publicvoid Dispose()
 {
 context.Dispose();
 }
}

可以看到,這裡有一些我們需要測試的東西,即這些方法:

//Syncvoid Insert(string url);
IEnumerable<Post> GetAll();
IEnumerable<Post> GetAll(Expression<Func<Post, bool>> filter);
Post FindById(int id);//AsyncTask<bool> InsertAsync(string url);
Task<List<Post>> GetAllAsync();
Task<Post> FindByIdAsync(int id);

現在我們已經知道我們試圖測試的樣子了,讓我們來看看它的他移動部件。

存儲庫文件

我傾向於擁有這種倉庫。 這裡有幾個需要注意的事項:

  • 它是通用庫,因此可以針對多種實體類型使用( 如果需要專門存儲庫,則需要從一般存儲庫轉移到簡單繼承自 Repository<T>的特定存儲庫)。
  • 它可以與一個工作單元一起使用,抽象
  • 它可以用於包含 DbContext 導航屬性
publicclass Repository<T> : IRepository<T> where T : class, IEntity 
{
 privatereadonly IUnitOfWork context;
 public Repository(IUnitOfWork context)
 {
 this.context = context;
 }
 #region Syncpublicint Count()
 {
 return context.Get<T>().Count(); 
 }
 publicvoid Add(T item)
 {
 context.Add(item);
 }
 publicbool Contains(T item)
 {
 return context.Get<T>().FirstOrDefault(t => t == item)!= null;
 }
 publicbool Remove(T item)
 {
 return context.Remove(item);
 }
 public T Get(int id)
 {
 return context.Get<T>().SingleOrDefault(x => x.Id == id);
 }
 public T GetIncluding(
 int id, 
 params Expression<Func<T, object>>[] includeProperties)
 {
 return GetAllIncluding(includeProperties).SingleOrDefault(x => x.Id == id);
 }
 public IQueryable<T> GetAll()
 {
 return context.Get<T>();
 }
 public IQueryable<T> GetAll(Expression<Func<T, bool>> predicate)
 {
 return context.Get<T>().Where(predicate).AsQueryable<T>();
 }
 ///<summary>/// Used for Lazyloading navigation properties///</summary>public IQueryable<T> GetAllIncluding(
 params Expression<Func<T, object>>[] includeProperties)
 {
 IQueryable<T> queryable = GetAll();
 foreach (Expression<Func<T, object>> includeProperty in includeProperties)
 {
 queryable = queryable.Include(includeProperty);
 }
 return queryable;
 }
 #endregion#region Asyncpublicasync Task<int> CountAsync()
 {
 returnawait Task.Run(() => context.Get<T>().Count()); 
 }
 public Task<bool> AddAsync(T item)
 {
 return Task.Run(() => {
 context.Add(item);
 returntrue;
 });
 }
 public Task<bool> ContainsAsync(T item)
 {
 return Task.Run(
 () => context.Get<T>().FirstOrDefault(t => t == item)!= null);
 }
 public Task<bool> RemoveAsync(T item)
 {
 return Task.Run(() => context.Remove(item));
 }
 public Task<T> GetAsync(int id)
 {
 return Task.Run(
 () => context.Get<T>().SingleOrDefault(x => x.Id == id));
 }
 publicasync Task<T> GetIncludingAsync(
 int id, 
 params Expression<Func<T, object>>[] includeProperties)
 {
 IQueryable<T> queryable = await GetAllIncludingAsync(includeProperties);
 returnawait queryable.SingleOrDefaultAsync(x => x.Id == id);
 }
 public Task<IQueryable<T>> GetAllAsync()
 {
 return Task.Run(() => context.Get<T>());
 }
 public Task<IQueryable<T>> GetAllAsync(
 Expression<Func<T, bool>> predicate)
 {
 return Task.Run(() => 
 context.Get<T>().Where(predicate).AsQueryable<T>());
 }
 ///<summary>/// Used for Lazyloading navigation properties///</summary>public Task<IQueryable<T>> GetAllIncludingAsync(
 params Expression<Func<T, object>>[] includeProperties)
 {
 return Task.Run(
 () => {
 IQueryable<T> queryable = GetAll();
 foreach (Expression<Func<T, object>> includeProperty in includeProperties)
 {
 queryable = queryable.Include(includeProperty);
 }
 return queryable;
 });
 }
 #endregion}

Abstraction

就像我所說的,上面展示的存儲庫代碼依賴於一個工作單元( 抽象抽象)。 那麼這個抽象到底是什麼。 很簡單,這是一個 Entity Framework DbContext,只是我們不會直接使用它,我們會使用上面顯示的Respository來獲取/插入數據。 我已經指出,使用工作原理單元的Having的一個好處是,我們可以在一個事務中提交幾個存儲庫操作。 這裡是這個例子使用的工作單元的代碼

publicabstractclass EfDataContextBase : DbContext, IUnitOfWork
{
 public EfDataContextBase(string nameOrConnectionString)
 : base(nameOrConnectionString)
 {
 }
 public IQueryable<T> Get<T>() where T : class {
 return Set<T>();
 }
 publicbool Remove<T>(T item) where T : class {
 try {
 Set<T>().Remove(item);
 }
 catch (Exception)
 {
 returnfalse;
 }
 returntrue;
 }
 publicnewint SaveChanges()
 {
 returnbase.SaveChanges();
 }
 publicvoid Attach<T>(T obj) where T : class {
 Set<T>().Attach(obj);
 }
 publicvoid Add<T>(T obj) where T : class {
 Set<T>().Add(obj);
 }
}publicclass RepositoryExampleSachaTestContext : EfDataContextBase, ISachaContext
{
 public RepositoryExampleSachaTestContext(string nameOrConnectionString)
 : base(nameOrConnectionString)
 {
 this.Configuration.LazyLoadingEnabled = true;
 this.Configuration.ProxyCreationEnabled = false;
 }
 public DbSet<Post> Posts { get; set; }
 publicvoid DoSomethingDirectlyWithDatabase()
 {
 //Not done for this example }
}

IOC使用存儲庫插件連接。

為了正確地連接所有這些,我使用了IOC容器。 我選擇使用 Autofac。 實際上,IOC代碼是附加的,並沒有什麼意義,但是我應該在這裡包括完整性:

publicclass IOCManager
{
 privatestatic IOCManager instance;
 static IOCManager()
 {
 instance = new IOCManager();
 }
 private IOCManager()
 {
 var builder = new ContainerBuilder();
 // Register individual components builder.RegisterType<RepositoryExampleSachaTestContext>()
. As<IUnitOfWork>()
. WithParameter("nameOrConnectionString", "SachaTestContextConnection")
. InstancePerLifetimeScope();
 builder.RegisterType<SomeService>()
. As<ISomeService>().InstancePerLifetimeScope();
 builder.RegisterGeneric(typeof(Repository<>))
. As(typeof(IRepository<>))
. InstancePerLifetimeScope();
 Container = builder.Build();
 }
 public IContainer Container { get; privateset; }
 publicstatic IOCManager Instance
 {
 get {
 return instance;
 }
 }
}

使用存儲庫進行測試。

現在我們已經看到了我們要測試的系統的所有部分,所以現在讓我們看一些測試用例。 在所有這些測試中,我將使用最小化的模擬庫

使用存儲庫管理器插入( )

下面是我們可能模擬一個通過存儲庫發生的插入。 顯然如果代碼依賴插入的Id,那麼你需要擴展它,也許提供一個回調更新添加的Post,如果你的代碼有興趣的話。

這是我們要模擬的代碼:

publicvoid Insert(string url)
{
 Post post = new Post() { Url = url };
 post.PostComments.Add(new PostComment()
 {
 Comment = string.Format("yada yada {0}", counter++)
 });
 repository.Add(post);
}public Task<bool> InsertAsync(string url)
{
 Post post = new Post() { Url = url };
 post.PostComments.Add(new PostComment()
 {
 Comment = string.Format("yada yada {0}", counter++)
 });
 return repository.AddAsync(post);
}

你可以看到下面的測試代碼的同步版本和asynchrounous版本

[TestCase]publicvoid TestInsert()
{
 Mock<IUnitOfWork> uowMock = new Mock<IUnitOfWork>();
 Mock<IRepository<Post>> repoMock = new Mock<IRepository<Post>>();
 SomeService service = new SomeService(uowMock.Object, repoMock.Object);
 service.Insert("TestInsert");
 repoMock.Verify(m => m.Add(It.IsAny<Post>()), Times.Once());
}
[TestCase]publicasyncvoid TestInsertAsync()
{
 Mock<IUnitOfWork> uowMock = new Mock<IUnitOfWork>();
 Mock<IRepository<Post>> repoMock = new Mock<IRepository<Post>>();
 repoMock.Setup(x => x.AddAsync(It.IsAny<Post>())).Returns(Task.FromResult(true));
 SomeService service = new SomeService(uowMock.Object, repoMock.Object);
 await service.InsertAsync("TestInsertAsync");
 repoMock.Verify(m => m.AddAsync(It.IsAny<Post>()), Times.Once());
}

使用存儲庫提供了所有的服務

下面是我們可能模擬通過存儲庫發生的GetAll() 調用的方法。 可以看到,我們可以簡單地返回一些虛擬 Post 對象。

這是我們要模擬的代碼:

public IEnumerable<Post> GetAll()
{
 return repository.GetAll();
}publicasync Task<List<Post>> GetAllAsync()
{
 var posts = await repository.GetAllAsync();
 return posts.ToList();
}

你可以看到下面的同步和asynchrounous版本

[TestCase]publicvoid TestGetAll()
{
 Mock<IUnitOfWork> uowMock = new Mock<IUnitOfWork>();
 Mock<IRepository<Post>> repoMock = new Mock<IRepository<Post>>();
 var posts = Enumerable.Range(0, 5)
. Select(x =>new Post()
 {
 Url = string.Format("www.someurl{0}", x)
 }).ToList();
 repoMock.Setup(x => x.GetAll()).Returns(posts.AsQueryable());
 SomeService service = new SomeService(uowMock.Object, repoMock.Object);
 var retrievedPosts = service.GetAll();
 repoMock.Verify(m => m.GetAll(), Times.Once());
 CollectionAssert.AreEqual(posts, retrievedPosts);
}
[TestCase]publicasyncvoid TestGetAllAsync()
{
 Mock<IUnitOfWork> uowMock = new Mock<IUnitOfWork>();
 Mock<IRepository<Post>> repoMock = new Mock<IRepository<Post>>();
 var posts = Enumerable.Range(0, 5).Select(x =>new Post()
 {
 Id = x,
 Url = string.Format("www.someurl{0}", x)
 }).ToList();
 repoMock.Setup(x => x.GetAllAsync()).Returns(Task.FromResult(posts.AsQueryable()));
 SomeService service = new SomeService(uowMock.Object, repoMock.Object);
 var retrievedPosts = await service.GetAllAsync();
 repoMock.Verify(m => m.GetAllAsync(), Times.Once());
 CollectionAssert.AreEqual(posts, retrievedPosts);
}

我們提供了一個表達式,其中我們提供一個表達式 <<Post,bool>> 過濾器,使用倉庫

我所發布的存儲庫代碼的另一個東西是使用 Expression<Func<Post,bool>> 將篩選器應用於 IQueryable<Post>

這是我們要模擬的代碼:

public IEnumerable<Post> GetAll(Expression<Func<Post, bool>> filter)
{
 return repository.GetAll(filter);
}

那麼我們如何編寫測試代碼來實現。 實際上,我們需要做的就是搞懂模擬,並確保在執行任何斷言之前應用過濾器。

[TestCase]publicvoid TestGetAllWithLambda()
{
 Mock<IUnitOfWork> uowMock = new Mock<IUnitOfWork>();
 Mock<IRepository<Post>> repoMock = new Mock<IRepository<Post>>();
 var posts = Enumerable.Range(0, 5)
. Select(x =>new Post()
 {
 Url = string.Format("www.someurl{0}", x)
 }).ToList();
 for (int i = 0; i < posts.Count; i++)
 {
 posts[i].PostComments.Add(new PostComment()
 {
 Comment = string.Format("some test comment {0}", i)
 });
 }
 repoMock.Setup(moq => moq.GetAll(It.IsAny<Expression<Func<Post, bool>>>()))
. Returns((Expression<Func<Post, bool>> predicate) => 
 posts.Where(predicate.Compile()).AsQueryable());
 SomeService service = new SomeService(uowMock.Object, repoMock.Object);
 Func<Post, bool> func = (x) => x.Url == "www.someurl1";
 Expression<Func<Post, bool>> filter = post => func(post);
 var retrievedPosts = service.GetAll(filter);
 CollectionAssert.AreEqual(posts.Where(func), retrievedPosts);
}

F indById (-) 使用存儲庫

下面是我們可能模擬通過存儲庫發生的FindById() 調用的方法。 可以看到,我們可以簡單地返回虛擬 Post 對象。

這是我們要模擬的代碼:

public Post FindById(int id)
{
 var post = repository.Get(id);
 return post;
}public Task<Post> FindByIdAsync(int id)
{
 return repository.GetIncludingAsync(id, x => x.PostComments);
}

你可以看到下面的同步和asynchrounous版本

[TestCase]publicvoid TestFindById()
{
 Mock<IUnitOfWork> uowMock = new Mock<IUnitOfWork>();
 Mock<IRepository<Post>> repoMock = new Mock<IRepository<Post>>();
 var posts = Enumerable.Range(0, 5).Select(x =>new Post()
 {
 Id = x,
 Url = string.Format("www.someurl{0}", x)
 }).ToList();
 for (int i = 0; i < posts.Count; i++)
 {
 posts[i].PostComments.Add(new PostComment()
 {
 Comment = string.Format("some test comment {0}", i)
 });
 }
 repoMock.Setup(moq => moq.Get(It.IsInRange(0, 5, Range.Inclusive)))
. Returns((int id) => posts.SingleOrDefault(x => x.Id == id));
 SomeService service = new SomeService(uowMock.Object, repoMock.Object);
 var retrievedPost = service.FindById(2);
 Assert.AreEqual(2, retrievedPost.Id);
}
[TestCase]publicasyncvoid TestFindByIdAsync()
{
 Mock<IUnitOfWork> uowMock = new Mock<IUnitOfWork>();
 Mock<IRepository<Post>> repoMock = new Mock<IRepository<Post>>();
 var posts = Enumerable.Range(0, 5).Select(x =>new Post()
 {
 Id = x,
 Url = string.Format("www.someurl{0}", x)
 }).ToList();
 for (int i = 0; i < posts.Count; i++)
 {
 posts[i].PostComments.Add(new PostComment()
 {
 Comment = string.Format("some test comment {0}", i)
 });
 }
 repoMock.Setup(moq => moq.GetIncludingAsync(
 It.IsInRange(0, 5, Range.Inclusive), 
 new[] { It.IsAny<Expression<Func<Post, object>>>() }))
. Returns(
 (int id, Expression<Func<Post, object>>[] includes) => 
 Task.FromResult(posts.SingleOrDefault(x => x.Id == id)));
 SomeService service = new SomeService(uowMock.Object, repoMock.Object);
 var retrievedPost = await service.FindByIdAsync(2);
 Assert.AreEqual(2, retrievedPost.Id);
}

以下是所有工作都能正常運行的證明:

測試使用 Entity Framework
  • 請參閱:EFTest.csproj
  • 請參閱:EFTest.Tests. csproj

我們已經看到,我們確實可以使用一個存儲庫來測試我們的數據訪問代碼。 不過就像我說的,Entity Framework 團隊發布了一個博客帖子( https://msdn.microsoft.com/en-us/data/dn314429) 聲明允許我們在代碼中直接使用 Entity Framework DbContext,而且仍然很容易使用 mock/測試 double。 Naturually我想試試這個我們來吧

本節討論使用 DbContext 抽象( 你仍然希望使用抽象,這樣可以對它的中的任何直接 DbContext.Database 調用進行模擬和測試)。

LazyLoading

Entity Framework 允許你關閉延遲載入。 當你這樣做的時候,你就可以自己選擇導航屬性了。 實際的代碼包含 延遲載入/非 延遲載入 示例,但是我選擇僅覆蓋測試的非 延遲載入 版本。

正在測試的系統( ( 關聯) )

這是我們將要測試的類:

publicclass SomeService : ISomeService, IDisposable
{
 privatereadonly ISachaContext context;
 privateint counter;
 public SomeService(ISachaContext context)
 {
 this.context = context;
 }
 publicvoid Insert(string url)
 {
 Post post = new Post() { Url = url };
 post.PostComments.Add(new PostComment()
 {
 Comment = string.Format("yada yada {0}", counter++)
 });
 context.Posts.Add(post);
 }
 public IEnumerable<Post> GetAll()
 {
 return context.Posts.AsEnumerable();
 }
 public IEnumerable<Post> GetAll(Expression<Func<Post, bool>> filter)
 {
 return context.Posts.Where(filter).AsEnumerable();
 }
 public Post FindById(int id)
 {
 //NOTE : Even if you included a line like the one below it would include //the PostComments, which seems to be NonLazy//this is due to the fact that the Post(s) and Comment(s) are already in the Context//var post1 = context.Posts.FirstOrDefault(p => p.Id == id);//This should show that we are not doing Lazy Loading and DO NEED to use //Include for navigation propertiesvar postWithNoCommentsProof = context.Posts.FirstOrDefault();
 var postWithCommentsThanksToInclude = context.Posts
. Include(x => x.PostComments).FirstOrDefault();
 var post = context.Posts.Where(p => p.Id == id)
. Include(x => x.PostComments).FirstOrDefault();
 return post;
 }
 publicasync Task<bool> InsertAsync(string url)
 {
 Post post = new Post() { Url = url };
 post.PostComments.Add(new PostComment()
 {
 Comment = string.Format("yada yada {0}", counter++)
 });
 context.Posts.Add(post);
 returntrue;
 }
 publicasync Task<List<Post>> GetAllAsync()
 {
 returnawait context.Posts.ToListAsync(); 
 }
 publicasync Task<Post> FindByIdAsync(int id)
 {
 //NOTE : Even if you included a line like the one below it would include //the PostComments, which seems to be NonLazy//this is due to the fact that the Post(s) and Comment(s) are already in the Context//var post1 = context.Posts.FirstOrDefault(p => p.Id == id);//This should show that we are not doing Lazy Loading and DO NEED to use //Include for navigation propertiesvar postWithNoCommentsProof = await context.Posts.FirstOrDefaultAsync();
 var postWithCommentsThanksToInclude = await context.Posts
. Include(x => x.PostComments).FirstOrDefaultAsync();
 var post = await context.Posts.Where(p => p.Id == id)
. Include(x => x.PostComments).FirstOrDefaultAsync();
 return post;
 }
 publicvoid Dispose()
 {
 context.Dispose();
 }
}

IOC連接

為了正確地連接所有這些,我使用了IOC容器。 我選擇使用 Autofac。 實際上,IOC代碼是附加的,並沒有什麼意義,但是我應該在這裡包括完整性:

publicclass IOCManager
{
 privatestatic IOCManager instance;
 static IOCManager()
 {
 instance = new IOCManager();
 }
 private IOCManager()
 {
 var builder = new ContainerBuilder();
 // Register individual components builder.RegisterType<SachaContext>()
. As<ISachaContext>()
. WithParameter("nameOrConnectionString", "SachaTestContextConnection")
. InstancePerLifetimeScope();
 builder.RegisterType<SachaLazyContext>()
. As<ISachaLazyContext>()
. WithParameter("nameOrConnectionString", "SachaTestContextConnection")
. InstancePerLifetimeScope();
 builder.RegisterType<SomeService>()
. As<ISomeService>().InstancePerLifetimeScope();
 builder.RegisterType<SomeServiceLazy>()
. As<ISomeServiceLazy>().InstancePerLifetimeScope();
 Container = builder.Build();
 }
 public IContainer Container { get; privateset; }
 publicstatic IOCManager Instance
 {
 get {
 return instance;
 }
 }
}

測試失敗。

現在我們已經看到了我們要測試的系統的所有部分,所以現在讓我們看一些測試用例。 在所有這些測試中,我將使用最小化的模擬庫

DbContext測試雙精度

為了嘗試提供在 Entity Framework 博客上提供的建議,我們需要確保使用 DbContext的測試雙重 DbSet。 下面是本文使用的內容:

publicclass SachaContextTestDouble : DbContext, ISachaContext
{
 publicvirtual DbSet<Post> Posts { get; set; }
 publicvoid DoSomethingDirectlyWithDatabase()
}

非同步版本

下面顯示的直接 Entity Framework 代碼的非同步版本使用了一些 helper 類,如 Entity Framework 博客 https://msdn.microsoft.com/en-us/data/dn314429 所示,我在下面展示了完整性:

using System.Collections.Generic;using System.Data.Entity.Infrastructure;using System.Linq;using System.Linq.Expressions;using System.Threading;using System.Threading.Tasks;namespace EFTest.Tests
{internalclass TestDbAsyncQueryProvider<TEntity> : IDbAsyncQueryProvider
{
 privatereadonly IQueryProvider _inner;
 internal TestDbAsyncQueryProvider(IQueryProvider inner)
 {
 _inner = inner;
 }
 public IQueryable CreateQuery(Expression expression)
 {
 returnnew TestDbAsyncEnumerable<TEntity>(expression);
 }
 public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
 {
 returnnew TestDbAsyncEnumerable<TElement>(expression);
 }
 publicobject 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));
 }
}internalclass 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()
 {
 returnnew TestDbAsyncEnumerator<T>(this.AsEnumerable().GetEnumerator());
 }
 IDbAsyncEnumerator IDbAsyncEnumerable.GetAsyncEnumerator()
 {
 return GetAsyncEnumerator();
 }
 IQueryProvider IQueryable.Provider
 {
 get { returnnew TestDbAsyncQueryProvider<T>(this); }
 }
}internalclass TestDbAsyncEnumerator<T> : IDbAsyncEnumerator<T>
{
 privatereadonly IEnumerator<T> _inner;
 public TestDbAsyncEnumerator(IEnumerator<T> inner)
 {
 _inner = inner;
 }
 publicvoid 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; }
 }
}

插入( )

下面是我們可能模擬通過直接 Entity Framework 使用而發生的插入。 顯然如果代碼依賴插入的Id,那麼你需要擴展它,也許提供一個回調更新添加的Post,如果你的代碼有興趣的話。

這是我們要模擬的代碼:

publicvoid Insert(string url)
{
 Post post = new Post() { Url = url };
 post.PostComments.Add(new PostComment()
 {
 Comment = string.Format("yada yada {0}", counter++)
 });
 context.Posts.Add(post);
}publicasync Task<bool> InsertAsync(string url)
{
 Post post = new Post() { Url = url };
 post.PostComments.Add(new PostComment()
 {
 Comment = string.Format("yada yada {0}", counter++)
 });
 context.Posts.Add(post);
 returntrue;
}

你可以看到下面的同步和asynchrounous版本

privatestatic Mock<DbSet<T>> CreateMockSet<T>(IQueryable<T> dataForDbSet) where T : class{
 var dbsetMock = new Mock<DbSet<T>>();
 dbsetMock.As<IQueryable<T>>().Setup(m => m.Provider)
. Returns(dataForDbSet.Provider);
 dbsetMock.As<IQueryable<T>>().Setup(m => m.Expression)
. Returns(dataForDbSet.Expression);
 dbsetMock.As<IQueryable<T>>().Setup(m => m.ElementType)
. Returns(dataForDbSet.ElementType);
 dbsetMock.As<IQueryable<T>>().Setup(m => m.GetEnumerator())
. Returns(dataForDbSet.GetEnumerator());
 return dbsetMock;
}
[TestCase]publicvoid TestInsert()
{
 var dbsetMock = new Mock<DbSet<Post>>();
 var uowMock = new Mock<SachaContextTestDouble>();
 uowMock.Setup(m => m.Posts).Returns(dbsetMock.Object); 
 var service = new SomeService(uowMock.Object);
 service.Insert("Some url");
 dbsetMock.Verify(m => m.Add(It.IsAny<Post>()), Times.Once()); 
}

注意:它可以看到 Entity Framework 模擬,我們需要創建一個模擬 DbSet,這是在這裡所有示例中使用的。 在使用標準LINQ對象表達式樹和LINQ提供程序時,我們使用了一個小技巧

(-)

下面是我們可能模擬通過直接 Entity Framework 使用而發生的GetAll() 調用。 可以看到,我們可以簡單地返回一些虛擬 Post 對象。

這是我們要模擬的代碼:

public IEnumerable<Post> GetAll()
{
 return context.Posts.AsEnumerable();
}publicasync Task<List<Post>> GetAllAsync()
{
 returnawait context.Posts.ToListAsync(); 
}

你可以看到下面的同步和asynchrounous版本

privatestatic Mock<DbSet<T>> CreateMockSet<T>(IQueryable<T> dataForDbSet) where T : class{
 var dbsetMock = new Mock<DbSet<T>>();
 dbsetMock.As<IQueryable<T>>().Setup(m => m.Provider)
. Returns(dataForDbSet.Provider);
 dbsetMock.As<IQueryable<T>>().Setup(m => m.Expression)
. Returns(dataForDbSet.Expression);
 dbsetMock.As<IQueryable<T>>().Setup(m => m.ElementType)
. Returns(dataForDbSet.ElementType);
 dbsetMock.As<IQueryable<T>>().Setup(m => m.GetEnumerator())
. Returns(dataForDbSet.GetEnumerator());
 return dbsetMock;
}
[TestCase]publicvoid TestGetAll()
{
 var posts = Enumerable.Range(0, 5).Select(
 x =>new Post()
 {
 Url = string.Format("www.someurl{0}", x)
 }).AsQueryable();
 var dbsetMock = CreateMockSet(posts);
 var mockContext = new Mock<SachaContextTestDouble>();
 mockContext.Setup(c => c.Posts).Returns(dbsetMock.Object);
 var service = new SomeService(mockContext.Object);
 var retrievedPosts = service.GetAll().ToList();
 var postsList = posts.ToList();
 Assert.AreEqual(posts.Count(), retrievedPosts.Count());
 Assert.AreEqual(postsList[0].Url, retrievedPosts[0].Url);
 Assert.AreEqual(postsList[4].Url, retrievedPosts[4].Url);
}
[TestCase]publicasync Task TestGetAllAsync()
{
 var posts = Enumerable.Range(0, 5).Select(
 x =>new Post()
 {
 Url = string.Format("www.someurl{0}", x)
 }).AsQueryable();
 var dbsetMock = new Mock<DbSet<Post>>();
 dbsetMock.As<IDbAsyncEnumerable<Post>>()
. Setup(m => m.GetAsyncEnumerator())
. Returns(new TestDbAsyncEnumerator<Post>(posts.GetEnumerator()));
 dbsetMock.As<IQueryable<Post>>()
. Setup(m => m.Provider)
. Returns(new TestDbAsyncQueryProvider<Post>(posts.Provider));
 dbsetMock.As<IQueryable<Post>>().Setup(m => m.Expression).Returns(posts.Expression);
 dbsetMock.As<IQueryable<Post>>().Setup(m => m.ElementType).Returns(posts.ElementType);
 dbsetMock.As<IQueryable<Post>>().Setup(m => m.GetEnumerator()).Returns(posts.GetEnumerator());
 var mockContext = new Mock<SachaContextTestDouble>();
 mockContext.Setup(c => c.Posts).Returns(dbsetMock.Object);
 var service = new SomeService(mockContext.Object);
 var retrievedPosts = await service.GetAllAsync();
 var postsList = posts.ToList();
 Assert.AreEqual(posts.Count(), retrievedPosts.Count());
 Assert.AreEqual(postsList[0].Url, retrievedPosts[0].Url);
 Assert.AreEqual(postsList[4].Url, retrievedPosts[4].Url);
}

我們提供一個表達式 <函數 <Post,bool>> 過濾器( Filter )。

我們也可以利用 Expression<Func<Post,bool>> 將篩選器應用於 IQueryable<Post>

這是我們要模擬的代碼:

public IEnumerable<Post> GetAll(Expression<Func<Post, bool>> filter)
{
 return context.Posts.Where(filter).AsEnumerable();
}

那麼我們如何編寫測試代碼來實現。 實際上,我們需要做的就是搞懂模擬,並確保在執行任何斷言之前應用過濾器。

privatestatic Mock<DbSet<T>> CreateMockSet<T>(IQueryable<T> dataForDbSet) where T : class{
 var dbsetMock = new Mock<DbSet<T>>();
 dbsetMock.As<IQueryable<T>>().Setup(m => m.Provider)
. Returns(dataForDbSet.Provider);
 dbsetMock.As<IQueryable<T>>().Setup(m => m.Expression)
. Returns(dataForDbSet.Expression);
 dbsetMock.As<IQueryable<T>>().Setup(m => m.ElementType)
. Returns(dataForDbSet.ElementType);
 dbsetMock.As<IQueryable<T>>().Setup(m => m.GetEnumerator())
. Returns(dataForDbSet.GetEnumerator());
 return dbsetMock;
}
[TestCase]publicvoid TestGetAllWithLambda()
{
 var posts = Enumerable.Range(0, 5).Select(x =>new Post()
 {
 Url = string.Format("www.someurl{0}", x)
 }).ToList();
 for (int i = 0; i < posts.Count; i++)
 {
 posts[i].PostComments.Add(new PostComment()
 {
 Comment = string.Format("some test comment {0}", i)
 });
 }
 var queryablePosts = posts.AsQueryable();
 var dbsetMock = CreateMockSet(queryablePosts);
 var mockContext = new Mock<SachaContextTestDouble>();
 mockContext.Setup(c => c.Posts).Returns(dbsetMock.Object);
 var service = new SomeService(mockContext.Object);
 Func<Post, bool> func = (x) => x.Url == "www.someurl1";
 Expression<Func<Post, bool>> filter = post => func(post);
 var retrievedPosts = service.GetAll(filter);
 CollectionAssert.AreEqual(posts.Where(func).ToList(), retrievedPosts.ToList());
}

FindById ( )

下面是我們可能模擬通過直接 Entity Framework 使用發生的FindById() 調用。 可以看到,我們可以簡單地返回虛擬 Post 對象。

這是我們要模擬的代碼:

public Post FindById(int id)
{
 //NOTE : Even if you included a line like the one below it would include //the PostComments, which seems to be NonLazy//this is due to the fact that the Post(s) and Comment(s) are already in the Context//var post1 = context.Posts.FirstOrDefault(p => p.Id == id);//This should show that we are not doing Lazy Loading and DO NEED to use //Include for navigation propertiesvar postWithNoCommentsProof = context.Posts.FirstOrDefault();
 var postWithCommentsThanksToInclude = context.Posts
. Include(x => x.PostComments).FirstOrDefault();
 var post = context.Posts.Where(p => p.Id == id)
. Include(x => x.PostComments).FirstOrDefault();
 return post;
}publicasync Task<Post> FindByIdAsync(int id)
{
 //NOTE : Even if you included a line like the one below it would include //the PostComments, which seems to be NonLazy//this is due to the fact that the Post(s) and Comment(s) are already in the Context//var post1 = context.Posts.FirstOrDefault(p => p.Id == id);//This should show that we are not doing Lazy Loading and DO NEED to use //Include for navigation propertiesvar postWithNoCommentsProof = await context.Posts.FirstOrDefaultAsync();
 var postWithCommentsThanksToInclude = await context.Posts
. Include(x => x.PostComments).FirstOrDefaultAsync();
 var post = await context.Posts.Where(p => p.Id == id)
. Include(x => x.PostComments).FirstOrDefaultAsync();
 return post;
}

你可以看到下面的同步和asynchrounous版本

privatestatic Mock<DbSet<T>> CreateMockSet<T>(IQueryable<T> dataForDbSet) where T : class{
 var dbsetMock = new Mock<DbSet<T>>();
 dbsetMock.As<IQueryable<T>>().Setup(m => m.Provider)
. Returns(dataForDbSet.Provider);
 dbsetMock.As<IQueryable<T>>().Setup(m => m.Expression)
. Returns(dataForDbSet.Expression);
 dbsetMock.As<IQueryable<T>>().Setup(m => m.ElementType)
. Returns(dataForDbSet.ElementType);
 dbsetMock.As<IQueryable<T>>().Setup(m => m.GetEnumerator())
. Returns(dataForDbSet.GetEnumerator());
 return dbsetMock;
}
[TestCase]publicvoid TestFindById()
{
 var posts = Enumerable.Range(0, 5).Select(x =>new Post()
 {
 Id = x,
 Url = string.Format("www.someurl{0}", x)
 }).ToList();
 for (int i = 0; i < posts.Count; i++)
 {
 posts[i].PostComments.Add(new PostComment()
 {
 Comment = string.Format("some test comment {0}", i)
 });
 }
 var queryablePosts = posts.AsQueryable();
 var dbsetMock = CreateMockSet(queryablePosts);
 //NOTE : we need to use the string version of Include as the other one that accepts// an Expression tree is an extension method in System.Data.Entity.QueryableExtensions// which Moq doesn't like//// So the following will not work, as will result in this sort of Exception from Moq//// Expression references a method that does not belong to // the mocked object: m => m.Include<Post,IEnumerable`1>(It.IsAny<Expression`1>())//// dbsetMock.Setup(m => m.Include(It.IsAny<Expression<Func<Post,IEnumerable<PostComment>>>>()))//. Returns(dbsetMock.Object); dbsetMock.Setup(m => m.Include("PostComments")).Returns(dbsetMock.Object);
 var mockContext = new Mock<SachaContextTestDouble>();
 mockContext.Setup(c => c.Posts).Returns(dbsetMock.Object);
 var service = new SomeService(mockContext.Object);
 var retrievedPost = service.FindById(1);
 Assert.AreEqual(retrievedPost.Id,1);
 Assert.IsNotNull(retrievedPost.PostComments);
 Assert.AreEqual(retrievedPost.PostComments.Count,1);
}
[TestCase]publicasync Task TestFindByIdAsync()
{
 var posts = Enumerable.Range(0, 5).Select(x =>new Post()
 {
 Id = x,
 Url = string.Format("www.someurl{0}", x)
 }).ToList();
 for (int i = 0; i < posts.Count; i++)
 {
 posts[i].PostComments.Add(new PostComment()
 {
 Comment = string.Format("some test comment {0}", i)
 });
 }
 var queryablePosts = posts.AsQueryable();
 var dbsetMock = new Mock<DbSet<Post>>();
 dbsetMock.As<IDbAsyncEnumerable<Post>>()
. Setup(m => m.GetAsyncEnumerator())
. Returns(new TestDbAsyncEnumerator<Post>(queryablePosts.GetEnumerator()));
 dbsetMock.As<IQueryable<Post>>()
. Setup(m => m.Provider)
. Returns(new TestDbAsyncQueryProvider<Post>(queryablePosts.Provider));
 dbsetMock.As<IQueryable<Post>>().Setup(m => m.Expression).Returns(queryablePosts.Expression);
 dbsetMock.As<IQueryable<Post>>().Setup(m => m.ElementType).Returns(queryablePosts.ElementType);
 dbsetMock.As<IQueryable<Post>>().Setup(m => m.GetEnumerator()).Returns(queryablePosts.GetEnumerator());
 //NOTE : we need to use the string version of Include as the other one that accepts// an Expression tree is an extension method in System.Data.Entity.QueryableExtensions// which Moq doesn't like//// So the following will not work, as will result in this sort of Exception from Moq//// Expression references a method that does not belong to // the mocked object: m => m.Include<Post,IEnumerable`1>(It.IsAny<Expression`1>())//// dbsetMock.Setup(m => m.Include(It.IsAny<Expression<Func<Post,IEnumerable<PostComment>>>>()))//. Returns(dbsetMock.Object); dbsetMock.Setup(m => m.Include("PostComments")).Returns(dbsetMock.Object);
 var mockContext = new Mock<SachaContextTestDouble>();
 mockContext.Setup(c => c.Posts).Returns(dbsetMock.Object);
 var service = new SomeService(mockContext.Object);
 var retrievedPost = await service.FindByIdAsync(1);
 Assert.AreEqual(retrievedPost.Id, 1);
 Assert.IsNotNull(retrievedPost.PostComments);
 Assert.AreEqual(retrievedPost.PostComments.Count, 1);
}

以下是所有工作都能正常運行的證明:

結論

因這裡,你有了這一點,可以看到,這幾天確實可以直接使用 Entity Framework。 我希望這篇文章對你有用,並且可以幫助你測試你自己的數據訪問層。 如果你願意自己決定如何去做,我希望這對你來說是沒有意義的,這是你自己決定去的。



文章标签:rep  REPO  Repository  

Copyright © 2011 HelpLib All rights reserved.    知识分享协议 京ICP备05059198号-3  |  如果智培  |  酷兔英语