.net Entity Framework 4.1 的假 DbContext 进行测试
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/6904139/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Fake DbContext of Entity Framework 4.1 to Test
提问by Acaz Souza
I'm using this tutorial to Fake my DbContext and test: http://refactorthis.wordpress.com/2011/05/31/mock-faking-dbcontext-in-entity-framework-4-1-with-a-generic-repository/
我正在使用本教程来伪造我的 DbContext 并进行测试:http://refactorthis.wordpress.com/2011/05/31/mock-faking-dbcontext-in-entity-framework-4-1-with-a-generic -存储库/
But i have to change the FakeMainModuleContext implementation to use in my Controllers:
但我必须更改 FakeMainModuleContext 实现以在我的控制器中使用:
public class FakeQuestiona2011Context : IQuestiona2011Context
{
private IDbSet<Credencial> _credencial;
private IDbSet<Perfil> _perfil;
private IDbSet<Apurador> _apurador;
private IDbSet<Entrevistado> _entrevistado;
private IDbSet<Setor> _setor;
private IDbSet<Secretaria> _secretaria;
private IDbSet<Pesquisa> _pesquisa;
private IDbSet<Pergunta> _pergunta;
private IDbSet<Resposta> _resposta;
public IDbSet<Credencial> Credencial { get { return _credencial ?? (_credencial = new FakeDbSet<Credencial>()); } set { } }
public IDbSet<Perfil> Perfil { get { return _perfil ?? (_perfil = new FakeDbSet<Perfil>()); } set { } }
public IDbSet<Apurador> Apurador { get { return _apurador ?? (_apurador = new FakeDbSet<Apurador>()); } set { } }
public IDbSet<Entrevistado> Entrevistado { get { return _entrevistado ?? (_entrevistado = new FakeDbSet<Entrevistado>()); } set { } }
public IDbSet<Setor> Setor { get { return _setor ?? (_setor = new FakeDbSet<Setor>()); } set { } }
public IDbSet<Secretaria> Secretaria { get { return _secretaria ?? (_secretaria = new FakeDbSet<Secretaria>()); } set { } }
public IDbSet<Pesquisa> Pesquisa { get { return _pesquisa ?? (_pesquisa = new FakeDbSet<Pesquisa>()); } set { } }
public IDbSet<Pergunta> Pergunta { get { return _pergunta ?? (_pergunta = new FakeDbSet<Pergunta>()); } set { } }
public IDbSet<Resposta> Resposta { get { return _resposta ?? (_resposta = new FakeDbSet<Resposta>()); } set { } }
public void SaveChanges()
{
// do nothing (probably set a variable as saved for testing)
}
}
And my test like that:
我的测试是这样的:
[TestMethod]
public void IndexTest()
{
IQuestiona2011Context fakeContext = new FakeQuestiona2011Context();
var mockAuthenticationService = new Mock<IAuthenticationService>();
var apuradores = new List<Apurador>
{
new Apurador() { Matricula = "1234", Nome = "Acaz Souza Pereira", Email = "[email protected]", Ramal = "1234" },
new Apurador() { Matricula = "4321", Nome = "Samla Souza Pereira", Email = "[email protected]", Ramal = "4321" },
new Apurador() { Matricula = "4213", Nome = "Valderli Souza Pereira", Email = "[email protected]", Ramal = "4213" }
};
apuradores.ForEach(apurador => fakeContext.Apurador.Add(apurador));
ApuradorController apuradorController = new ApuradorController(fakeContext, mockAuthenticationService.Object);
ActionResult actionResult = apuradorController.Index();
Assert.IsNotNull(actionResult);
Assert.IsInstanceOfType(actionResult, typeof(ViewResult));
ViewResult viewResult = (ViewResult)actionResult;
Assert.IsInstanceOfType(viewResult.ViewData.Model, typeof(IndexViewModel));
IndexViewModel indexViewModel = (IndexViewModel)viewResult.ViewData.Model;
Assert.AreEqual(3, indexViewModel.Apuradores.Count);
}
I'm doing it right?
我做得对吗?
回答by Ladislav Mrnka
Unfortunately you are not doing it right because that article is wrong. It pretends that FakeContextwill make your code unit testable but it will not. Once you expose IDbSetor IQueryableto your controller and you fake the set with in memory collection you can never be sure that your unit test really tests your code. It is very easy to write a LINQ query in your controller which will pass your unit test (because FakeContextuses LINQ-to-Objects) but fails at runtime (because your real context uses LINQ-to-Entities). That makes whole purpose of your unit testing useless.
不幸的是,你没有做对,因为那篇文章是错误的。它假装这FakeContext将使您的代码单元可测试,但事实并非如此。一旦你暴露IDbSet或IQueryable到你的控制器并且你在内存集合中伪造了这个集合,你永远无法确定你的单元测试真的测试了你的代码。在您的控制器中编写 LINQ 查询非常容易,该查询将通过您的单元测试(因为FakeContext使用 LINQ-to-Objects)但在运行时失败(因为您的真实上下文使用 LINQ-to-Entities)。这使您的单元测试的整个目的毫无用处。
My opinion: Don't bother with faking context if you want to expose sets to controller. Instead use integration tests with real database for testing. That is the only way how to validate that LINQ queries defined in controller do what you expect.
我的意见:如果您想将集合公开给控制器,请不要担心伪造上下文。而是使用带有真实数据库的集成测试进行测试。这是验证控制器中定义的 LINQ 查询是否符合您的期望的唯一方法。
Sure, if you want to call just ToListor FirstOrDefaulton your sets your FakeContextwill serve you well but once you do anything more complex you can find a trap pretty soon (just put the string "Cannot be translated into a store expression"into Google - all these problems will appear only when you run Linq-to-entities but they will pass your tests with Linq-to-objects).
当然,如果你只想调用ToList或FirstOrDefault在你的集合中调用,你FakeContext会很好地为你服务,但是一旦你做任何更复杂的事情,你很快就会发现一个陷阱(只需将字符串“无法翻译成商店表达式”输入谷歌 - 所有这些只有在您运行 Linq-to-entities 时才会出现问题,但它们会通过您的 Linq-to-objects 测试)。
This is quite common question so you can check some other examples:
这是一个很常见的问题,因此您可以查看其他一些示例:
- To return IQueryable or not return IQueryable
- Unit Testing DbContext
- ASP.NET MVC3 and Entity Framework Code first architecture
- Organizationally, where should I put common queries when using Entity Framework Code First?
- Is it possible to stub Entity Framework context and classes to test data access layer?
回答by brentmckendrick
"Unfortunately you are not doing it right because that article is wrong. It pretends that FakeContext will make your code unit testable but it will not"
“不幸的是你没有做对,因为那篇文章是错误的。它假装 FakeContext 会让你的代码单元可测试,但它不会”
I am the creator of the blog post that you refer to. The problem I see here is a misunderstanding of the fundamentals of N-Layered unit testing. My post is not meant to be used directly to test controller logic.
我是您引用的博客文章的创建者。我在这里看到的问题是对 N 层单元测试的基本原理的误解。我的帖子不打算直接用于测试控制器逻辑。
A unit test should do exactly as the name implies and test 'One Unit'. If I am testing a controller (as you are doing above) I forget all about the data access. I should be removing all of the calls to database context in my mind and replacing them with a black box method call as if those operations were unknown to me. It is the code around those operations that I am interested in testing.
单元测试应该完全按照名称进行测试并测试“一个单元”。如果我正在测试控制器(正如您在上面所做的那样),我会忘记所有关于数据访问的信息。我应该删除我脑海中对数据库上下文的所有调用,并用黑盒方法调用替换它们,就好像这些操作对我来说是未知的。我感兴趣的是围绕这些操作的代码。
Example:
例子:
In my MVC application we use the repository pattern. I have a repository, say CustomerRepository : ICustomerRepository, which will perform all of my Customer database operations.
在我的 MVC 应用程序中,我们使用存储库模式。我有一个存储库,比如说 CustomerRepository : ICustomerRepository,它将执行我所有的 Customer 数据库操作。
If I were to test my controllers would I want the tests to test my repository, my database access, and the controller logic itself? of course not!there are many 'units' in this pipeline. What you want to do is create a fake repository which implements ICustomerRepository to allow you to test the controller logic in isolation.
如果我要测试我的控制器,我是否希望这些测试测试我的存储库、我的数据库访问和控制器逻辑本身?当然不是!这条管道中有许多“单位”。您想要做的是创建一个实现 ICustomerRepository 的假存储库,以允许您单独测试控制器逻辑。
This to the best of my knowledge cannot be done on the database context alone. (except maybe for using Microsoft Moles, which you can check out if you want). This is simply because all queries are performed outside of the context in your controller class.
据我所知,这不能单独在数据库上下文中完成。(除了可能使用 Microsoft Moles,如果需要,您可以查看)。这仅仅是因为所有查询都是在控制器类的上下文之外执行的。
If I wanted to test the CustomerRepository logic how would I do that? The easiest way is to use a fake context. This will allow me to make sure that when I'm trying to get a customer by id, it actually gets the customer by id and so forth. Repository methods are very simple and the "Cannot be translated into a store expression" problem will not usually surface. Though in some minor cases it may (sometimes due to incorrectly written linq queries) in these cases it is important to also perform integration tests that will test your code all the way through to the database. These problems will be found in integration testing. I have used this N-Layered technique for quite a while now and have found no problems with this.
如果我想测试 CustomerRepository 逻辑,我该怎么做?最简单的方法是使用假上下文。这将允许我确保当我尝试通过 id 获取客户时,它实际上是通过 id 获取客户等等。存储库方法非常简单,通常不会出现“无法转换为存储表达式”的问题。尽管在一些小情况下(有时是由于错误编写的 linq 查询)在这些情况下,执行集成测试也很重要,这些测试将测试您的代码一直到数据库。这些问题会在集成测试中发现。我已经使用这种 N-Layered 技术有一段时间了,并没有发现任何问题。
Integration Tests
集成测试
Obviously testing your app against the database itself is a costly exercise and once you get tens of thousands of tests it becomes a nightmare, on the other hand it best mimics how the code will be used in the 'real world'. These tests are important (from the ui to the database) also and they will be performed as part of the integration tests, NOT unit tests.
显然,针对数据库本身测试您的应用程序是一项代价高昂的工作,一旦您进行了数以万计的测试,它就会变成一场噩梦,另一方面,它最好地模仿了代码在“现实世界”中的使用方式。这些测试也很重要(从 ui 到数据库),它们将作为集成测试的一部分执行,而不是单元测试。
Acaz, what you really need in your scenario is a repository which is mockable/fakeable. If you wish to test your controllers as you are doing then your controller should be taking in an object which wraps the database functionality. Then it can return anything you need it to in order to test all aspects of your controller's functionality.
Acaz,你在你的场景中真正需要的是一个可模拟/可伪造的存储库。如果你想在你做的时候测试你的控制器,那么你的控制器应该接受一个包装数据库功能的对象。然后它可以返回您需要的任何内容,以测试控制器功能的所有方面。
see http://msdn.microsoft.com/en-us/library/ff714955.aspx
请参阅http://msdn.microsoft.com/en-us/library/ff714955.aspx
In order to test the repository itself (debated if needed in all cases) you will want to either fake the context or use something along the lines of the 'Moles' framework.
为了测试存储库本身(在所有情况下都需要讨论),您将需要伪造上下文或使用类似于“Moles”框架的内容。
LINQ is inherently hard to test. The fact that the query is defined outside of the context using extension methods gives us great flexibility but creates a testing nightmare. Wrap your context in a repository and this problem goes away.
LINQ 本质上很难测试。使用扩展方法在上下文之外定义查询这一事实为我们提供了极大的灵活性,但会造成测试噩梦。将您的上下文包装在存储库中,这个问题就会消失。
sorry so long :)
抱歉这么久:)
回答by Soe Moe
As Ladislav Mrnka mentioned, you should test Linq-to-Entity but not Linq-to-Object. I normally used Sql CE as testing DB and always recreate the database before each test. This may make test a little bit slow but so far I'm OK with the performance for my 100+ unit tests.
正如 Ladislav Mrnka 提到的,您应该测试 Linq-to-Entity 而不是 Linq-to-Object。我通常使用 Sql CE 作为测试数据库,并且总是在每次测试之前重新创建数据库。这可能会使测试有点慢,但到目前为止,我对 100 多个单元测试的性能还算满意。
First, change the connection string setting with SqlCe in the App.configof you test project.
首先,在您测试项目的App.config中使用 SqlCe 更改连接字符串设置。
<connectionStrings>
<add name="MyDbContext"
connectionString="Data Source=|DataDirectory|MyDb.sdf"
providerName="System.Data.SqlServerCe.4.0"
/>
</connectionStrings>
Second, set the db initializer with DropCreateDatabaseAlways.
其次,使用DropCreateDatabaseAlways设置数据库初始值设定项。
Database.SetInitializer<MyDbContext>(new DropCreateDatabaseAlways<MyDbContext>());
And Then, force EF to initialize before running each test.
然后,在运行每个测试之前强制 EF 进行初始化。
public void Setup() {
Database.SetInitializer<MyDbContext>(new DropCreateDatabaseAlways<MyDbContext>());
context = new MyDbContext();
context.Database.Initialize(force: true);
}
If you are using xunit, call Setup method in your constructor. If you are using MSTest, put TestInitializeAttribute on that method. If nunit.......
如果您使用的是 xunit,请在您的构造函数中调用 Setup 方法。如果您使用 MSTest,请将 TestInitializeAttribute 放在该方法上。如果 nunit ......
回答by user8128167
You can create a Fake DbContext by using Effort for EF 6+. See https://effort.codeplex.com/. Effort stands for Entity Framework Fake ObjectContext Realization Tool.
您可以使用 Effort for EF 6+ 创建 Fake DbContext。请参阅https://effort.codeplex.com/。努力代表éntity ˚Framework ˚FAKE ØbjectContext [Realization牛逼OOL。
For an article with a working sample, please see http://www.codeproject.com/Tips/1036630/Using-Effort-Entity-Framework-Unit-Testing-Toolor http://www.codeproject.com/Articles/460175/Two-strategies-for-testing-Entity-Framework-Effort?msg=5122027#xx5122027xx.
有关工作示例的文章,请参阅http://www.codeproject.com/Tips/1036630/Using-Effort-Entity-Framework-Unit-Testing-Tool或http://www.codeproject.com/Articles/ 460175/Two-strategies-for-testing-Entity-Framework-Effort?msg=5122027#xx5122027xx。
回答by Lomithrani
I know we shouldn't do it but sometimes you have to anyway (your boss might ask you too for instance and wouldn't change his mind).
我知道我们不应该这样做,但有时您无论如何都必须这样做(例如,您的老板可能也会问您并且不会改变主意)。
So as I had to do it I leave it here it might help some people. I'm quite new to c# / .net and all so it's far from being optimized/clean I suppose but it seems to work.
所以当我不得不这样做时,我把它留在这里它可能会帮助一些人。我对 c#/.net 还很陌生,所以我想它离优化/干净还很远,但它似乎有效。
following MSDN Find here the missing classand using a bit of reflection I managed to add one way properties : Key elements here are the AddNavigationPropertyand RefreshNavigationProperties. If anyone has suggestion to improve this code I'll gladly take them
遵循 MSDN在这里找到丢失的类并使用一些反射我设法添加了一种方式属性:这里的关键元素是AddNavigationProperty和RefreshNavigationProperties。如果有人建议改进此代码,我会很乐意接受他们
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Linq.Expressions;
namespace MockFactory
{
public class TestDbSet<TEntity> : DbSet<TEntity>, IQueryable, IEnumerable<TEntity>, IDbAsyncEnumerable<TEntity>
where TEntity : class
{
public readonly ObservableCollection<TEntity> _data;
private readonly IQueryable _query;
private readonly Dictionary<Type, object> entities;
public TestDbSet()
{
_data = new ObservableCollection<TEntity>();
_query = _data.AsQueryable();
entities = new Dictionary<Type, object>();
}
public override ObservableCollection<TEntity> Local
{
get { return _data; }
}
IDbAsyncEnumerator<TEntity> IDbAsyncEnumerable<TEntity>.GetAsyncEnumerator()
{
return new TestDbAsyncEnumerator<TEntity>(_data.GetEnumerator());
}
IEnumerator<TEntity> IEnumerable<TEntity>.GetEnumerator()
{
return _data.GetEnumerator();
}
Type IQueryable.ElementType
{
get { return _query.ElementType; }
}
Expression IQueryable.Expression
{
get { return _query.Expression; }
}
IQueryProvider IQueryable.Provider
{
get { return new TestDbAsyncQueryProvider<TEntity>(_query.Provider); }
}
IEnumerator IEnumerable.GetEnumerator()
{
return _data.GetEnumerator();
}
public void AddNavigationProperty<T>(DbSet<T> dbSet) where T : class
{
entities.Add(typeof (T), dbSet);
}
public void RefreshNavigationProperty(TEntity item)
{
foreach (var entity in entities)
{
var property = item.GetType().GetProperty(entity.Key.Name);
var type =
(int)item.GetType().GetProperty(entity.Key.Name.Replace(typeof(TEntity).Name, "")).GetValue(item);
var dbSets = (IEnumerable<object>)entity.Value.GetType().GetField("_data").GetValue(entity.Value);
var dbSet = dbSets.Single(x => (int)x.GetType().GetProperty("Id").GetValue(x) == type);
property.SetValue(item, dbSet);
}
}
public override TEntity Add(TEntity item)
{
RefreshNavigationProperty(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>();
}
}
}
You can then create your context
然后你可以创建你的上下文
public TestContext()
{
TypeUsers = new TestDbSet<TypeUser>();
StatusUsers = new TestDbSet<StatusUser>();
TypeUsers.Add(new TypeUser {Description = "FI", Id = 1});
TypeUsers.Add(new TypeUser {Description = "HR", Id = 2});
StatusUsers.Add(new StatusUser { Description = "Created", Id = 1 });
StatusUsers.Add(new StatusUser { Description = "Deleted", Id = 2 });
StatusUsers.Add(new StatusUser { Description = "PendingHR", Id = 3 });
Users = new TestDbSet<User>();
((TestDbSet<User>) Users).AddNavigationProperty(StatusUsers);
((TestDbSet<User>)Users).AddNavigationProperty(TypeUsers);
}
public override DbSet<TypeUser> TypeUsers { get; set; }
public override DbSet<StatusUser> StatusUsers { get; set; }
public override DbSet<User> Users { get; set; }
public int SaveChangesCount { get; private set; }
public override int SaveChanges(string modifierId)
{
SaveChangesCount++;
return 1;
}
}
Finally do not forget in your test to refresh the navigation properties before doing the assert (there should be a better way but I couldn't find it )
最后不要忘记在执行断言之前在测试中刷新导航属性(应该有更好的方法,但我找不到)
ContextFactory.Entity.Users.Each(((TestDbSet<User>) ContextFactory.Entity.Users).RefreshNavigationProperty);

