帮酷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  |  如果智培  |  酷兔英语