【问题标题】:Fake DbContext of Entity Framework 4.1 to TestEntity Framework 4.1 的假 DbContext 进行测试
【发布时间】:2011-08-01 20:36:32
【问题描述】:

我正在使用本教程来伪造我的 DbContext 并进行测试:http://refactorthis.wordpress.com/2011/05/31/mock-faking-dbcontext-in-entity-framework-4-1-with-a-generic-repository/

但我必须更改 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)
    }
}

我的测试是这样的:

[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 = "acaz@telecom.inf.br", Ramal = "1234" },
        new Apurador() { Matricula = "4321", Nome = "Samla Souza Pereira", Email = "samla@telecom.inf.br", Ramal = "4321" },
        new Apurador() { Matricula = "4213", Nome = "Valderli Souza Pereira", Email = "valderli@telecom.inf.br", 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);
}

我做得对吗?

【问题讨论】:

    标签: .net asp.net-mvc unit-testing entity-framework tdd


    【解决方案1】:

    很遗憾,您没有做对,因为那篇文章是错误的。它假装FakeContext 将使您的代码单元可测试,但它不会。一旦你将IDbSetIQueryable 暴露给你的控制器并且你在内存集合中伪造了集合,你永远无法确定你的单元测试真的测试了你的代码。在您的控制器中编写一个 LINQ 查询非常容易,它将通过您的单元测试(因为 FakeContext 使用 LINQ-to-Objects)但在运行时失败(因为您的真实上下文使用 LINQ-to-Entities)。这使您的单元测试的全部目的毫无用处。

    我的意见:如果您想将集合公开给控制器,请不要担心伪造上下文。而是使用带有真实数据库的集成测试进行测试。这是验证控制器中定义的 LINQ 查询是否符合您的预期的唯一方法。

    当然,如果你只想在你的集合上调用 ToListFirstOrDefault,你的 FakeContext 会很好地为你服务,但是一旦你做了更复杂的事情,你很快就会找到一个陷阱(只需输入字符串 “无法翻译成商店表达式” 输入 Google - 所有这些问题只有在您运行 Linq-to-entities 时才会出现,但它们会通过您使用 Linq-to-objects 的测试)。

    这是一个很常见的问题,因此您可以查看其他示例:

    【讨论】:

    • 这取决于您将如何使用存储库。
    • 我同意Brent's answer。正如 Ladislav 所解释的那样,在内存中伪造 DbSet 可能无法涵盖所有​​可能性,但它是一种有效的单元测试技术。我成功应用了方法described by Richard Garside。集成测试将确保端到端场景正常工作。请务必牢记,单元测试和集成测试是互补的,而不是相互排斥的替代方案。
    • 单元测试的重点是只测试那个单元。大概您将测试类似于 Web 服务中的业务逻辑的东西,该服务需要一个 DTO,对其进行验证,然后将其保存到数据库中。根据定义,您不必关心对象是否实际保存到存储库中,因为以后可能会发生变化。您只关心业务逻辑是否正确:是否对存储库进行了正确的调用,或者(如果对象验证失败)是否抛出了正确的错误,等等。
    • @HiredMind:我完全同意这一点,但最后你还需要测试,它会关心成功的查询执行或持久性。这都指向单一责任原则。如果您想在您的单元中测试业务逻辑,则该单元应该有执行该逻辑的单一职责 - 不执行数据库查询(甚至使用 LINQ 定义它们)。一旦你这样做了,你的方法就会很有吸引力,但是如果你将 L2E 查询直接放在与业务逻辑相同的单元中,你就会遇到麻烦。您需要集成测试来全面测试该查询和您的单元。
    • 最好的方法是不要使用假 DbSet 编写单元测试来测试您的编码逻辑,并且编写集成测试来测试组合解决方案在集成时是否真的有效?
    【解决方案2】:

    “很遗憾你没有做对,因为那篇文章是错误的。它假装 FakeContext 将使你的代码可单元测试,但它不会”

    我是您所引用的博客文章的创建者。我在这里看到的问题是对 N 层单元测试的基本原理的误解。我的帖子不打算直接用于测试控制器逻辑。

    单元测试应该完全顾名思义并测试“一个单元”。如果我正在测试一个控制器(就像你在上面所做的那样),我会忘记所有关于数据访问的事情。我应该在脑海中删除所有对数据库上下文的调用,并用黑盒方法调用替换它们,就好像这些操作对我来说是未知的一样。我有兴趣测试的是围绕这些操作的代码。

    示例:

    在我的 MVC 应用程序中,我们使用存储库模式。我有一个存储库,比如 CustomerRepository : ICustomerRepository,它将执行我所有的客户数据库操作。

    如果我要测试我的控制器,我是否希望这些测试能够测试我的存储库、我的数据库访问以及控制器逻辑本身?当然不是!这条管道中有很多“单元”。您要做的是创建一个实现 ICustomerRepository 的假存储库,以允许您单独测试控制器逻辑。

    据我所知,这不能仅在数据库上下文中完成。 (除了可能使用 Microsoft Moles,如果需要,您可以查看)。这仅仅是因为所有查询都是在控制器类的上下文之外执行的。

    如果我想测试 CustomerRepository 逻辑,我该怎么做?最简单的方法是使用假上下文。这将允许我确保当我尝试通过 id 获取客户时,它实际上是通过 id 获取客户等等。存储库方法非常简单,“无法翻译成存储表达式”问题通常不会出现。尽管在某些较小的情况下(有时是由于 linq 查询编写错误),在这些情况下,执行集成测试也很重要,这些测试将测试您的代码一直到数据库。这些问题会在集成测试中发现。我使用这种 N 层技术已经有一段时间了,没有发现任何问题。

    集成测试

    显然,针对数据库本身测试您的应用程序是一项昂贵的工作,一旦您进行了数万次测试,它就会变成一场噩梦,另一方面,它最好地模仿代码在“现实世界”中的使用方式。这些测试也很重要(从 ui 到数据库),它们将作为集成测试的一部分执行,而不是单元测试。

    Acaz,您在场景中真正需要的是一个可模拟/可伪造的存储库。如果您希望像您一样测试您的控制器,那么您的控制器应该接收一个包装数据库功能的对象。然后它可以返回您需要的任何东西,以便测试控制器功能的各个方面。

    http://msdn.microsoft.com/en-us/library/ff714955.aspx

    为了测试存储库本身(是否需要在所有情况下进行辩论),您将需要伪造上下文或使用类似于“Moles”框架的东西。

    LINQ 天生就很难测试。使用扩展方法在上下文之外定义查询这一事实为我们提供了极大的灵活性,但也带来了测试的噩梦。将您的上下文包装在存储库中,这个问题就消失了。

    抱歉这么久:)

    【讨论】:

    • 刚刚在 Microsoft ASP.NET MVC 上找到了这个,可能会比我解释得更好。迷茫的看一下! MSDN Unit Testing ASP.NET MVC
    • 我同意这个观点。 Richard Garside describes 一种用于实体框架数据上下文模拟的方法,非常干净且运行良好。
    • 为了测试存储库,您需要使用集成测试来验证您的查询和持久性逻辑,并使用单元测试来验证其他任何内容。用假上下文测试查询真的没有意义。当您需要与真正的提供者进行测试时,为什么还要使用假的测试?
    • 我同意这似乎是测试类型的混淆。我经常看到单元测试和集成测试重叠,这会导致开发人员进一步混淆。我更喜欢将集成和单元测试放在单独的测试项目中,以采用显式隔离。这种做法可能有助于减少团队中的混乱(读者不一定是 Acaz)。
    【解决方案3】:

    正如 Ladislav Mrnka 所说,您应该测试 Linq-to-Entity 而不是 Linq-to-Object。我通常使用 Sql CE 作为测试数据库,并且总是在每次测试之前重新创建数据库。这可能会使测试有点慢,但到目前为止,我的 100 多个单元测试的性能还不错。

    首先,在您的测试项目的 App.config 中使用 SqlCe 更改连接字符串设置。

    <connectionStrings>
        <add name="MyDbContext"
           connectionString="Data Source=|DataDirectory|MyDb.sdf"
             providerName="System.Data.SqlServerCe.4.0"
             />
    </connectionStrings>
    

    其次,使用DropCreateDatabaseAlways 设置数据库初始值设定项。

    Database.SetInitializer<MyDbContext>(new DropCreateDatabaseAlways<MyDbContext>());
    

    然后,强制 EF 在运行每个测试之前进行初始化。

    public void Setup() {
        Database.SetInitializer<MyDbContext>(new DropCreateDatabaseAlways<MyDbContext>());
    
        context = new MyDbContext();
        context.Database.Initialize(force: true);
    }
    

    如果您使用的是 xunit,请在构造函数中调用 Setup 方法。如果您使用的是 MSTest,请将 TestInitializeAttribute 放在该方法上。如果 nunit.......

    【讨论】:

    • 这为我节省了很多时间和精力(和维护......并且让我不必注入我的上下文)。绝对是任何中小型项目的最佳方式。
    • 针对 SQL Server 运行的所有查询是否也针对 SQL Server Ce 运行?
    • DropCreateDatabaseAlways 初始化程序总是在初始化时,你为什么要调用Database.Initialize()
    • @Elisa 是的,这样会更好。 :) 对于其他测试框架,我现在只在程序集初始化中创建数据库,并对每个测试使用事务范围。必须更快。 :)
    • 这就是我最终做的……经过许多小时的试验。我发现伪造 DbContext(最终伪造 IDbSet)并不值得。这是针对真实数据库(在我的情况下为 localdb)测试存储库的唯一真正方法。这种模糊了单元测试和集成测试之间的界限,因为尽管您正在对您的存储库进行单元测试,但它仍在测试较低层(真实上下文),例如集成测试。尽管如此,就我而言,它是我的仓库的“单元”测试,来自我的控制器的测试将是集成测试。
    【解决方案4】:

    您可以使用 Effort for EF 6+ 创建 Fake DbContext。见https://effort.codeplex.com/。努力代表E实体F框架Fake ObjectContext R实现T工具。

    有关工作示例的文章,请参阅http://www.codeproject.com/Tips/1036630/Using-Effort-Entity-Framework-Unit-Testing-Toolhttp://www.codeproject.com/Articles/460175/Two-strategies-for-testing-Entity-Framework-Effort?msg=5122027#xx5122027xx

    【讨论】:

      【解决方案5】:

      我知道我们不应该这样做,但有时你还是不得不这样做(例如,你的老板可能也会问你,但他不会改变主意)。

      所以我不得不这样做,所以我把它留在这里它可能会帮助一些人。我对 c# / .net 很陌生,所以我想它远未优化/干净,但它似乎有效。

      按照 MSDN Find here the missing class 并使用一点反射,我设法添加了单向属性:这里的关键元素是 AddNavigationPropertyRefreshNavigationProperties。如果有人有改进此代码的建议,我很乐意接受他们

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

      然后你可以创建你的上下文

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

      最后不要忘记在你的测试中在进行断言之前刷新导航属性(应该有更好的方法但我找不到它)

      ContextFactory.Entity.Users.Each(((TestDbSet<User>) ContextFactory.Entity.Users).RefreshNavigationProperty);
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-10-24
        • 1970-01-01
        相关资源
        最近更新 更多