52

My previous question让我再次思考层,存储库,依赖注入和像这样的架构。ASP.NET MVC3和实体框架代码第一架构

我的架构现在看起来像这样:
我首先使用EF代码,所以我只是创建了POCO类和上下文。这创建了数据库和模型。
级别更高的是业务层类(提供者)。我为每个域使用不同的提供者...像MemberProvider,RoleProvider,TaskProvider等,我正在这些提供者中创建我的DbContext的新实例。
然后我在我的控制器中实例化这些提供程序,获取数据并将它们发送到Views。

我最初的架构包含了仓库,我摆脱了仓库,因为我被告知它只是增加了复杂性,所以为什么我不仅仅使用EF。我想这样做..直接从控制器使用EF,但是我必须编写测试,而且它与真实数据库有点复杂。我不得不假冒 - 莫名其妙的数据。所以我为每个提供者做了一个接口,并在列表中伪造了硬编码数据。然后,我回到了某个地方,我不知道如何正确进行。

这些事情开始过于复杂得太快...许多方法和“pat”“......它创造了太多的噪音和无用的代码。

是否有任何简单和可测试的体系结构来创建实体框架和ASP.NET MVC3应用程序?

+0

你说库增加了“复杂”到你的应用程序,但我要说他们是最初的'开销',使测试更容易。嘲笑一些存储库比嘲笑整个数据上下文更容易。 – Omar 2011-04-10 03:40:12

+0

是的,但我不想在我目前的情况下开始的开销。我想快速申请进度。我已经失去了太多时间,没有任何实际进展。添加存储库会带来像IoC,DI等类似的东西。在我开始查看之前,我将不得不编写大量的测试。我知道这可能是正确的解决方案,但我不是在寻找“正确的”。我正在寻找简单的(虽然仍然可测试)的解决方案。 – Damb 2011-04-10 03:46:07

回答

94

如果您想要使用TDD(或任何其他高测试覆盖率的测试方法)和EF,您必须编写集成或端到端测试。这里的问题是,任何模拟上下文或存储库的方法都会创建测试,它可以测试您的上层逻辑(使用这些模拟)而不是您的应用程序。

简单的例子:

让我们定义通用的存储库:

public interface IGenericRepository<TEntity> 
{ 
    IQueryable<TEntity> GetQuery(); 
    ... 
} 

并让写一些商业方法:

public IEnumerable<MyEntity> DoSomethingImportant() 
{ 
    var data = MyEntityRepo.GetQuery().Select((e, i) => e); 
    ... 
} 

现在,如果你嘲笑的库中,您将使用的LINQ到 - 对象和你将有一个绿色的测试,但如果你使用Linq-To-Entities运行应用程序,你将会得到一个异常,因为在L2E中不支持索引的选择过载。

这是一个简单的例子,但在查询和其他常见错误中使用方法会发生同样的情况。此外,这也会影响像Add,Update和Delete这样的方法,这些方法通常暴露在资源库上如果你不写一个模拟真正模拟EF上下文和参照完整性的行为,你将不会测试你的实现。

故事的另一部分是懒惰加载的问题,单元测试对于嘲笑也很难检测到。

因此,您还应该引入集成或端到端测试,这些测试将使用真实EF环境L2E与真实数据库一起工作。顺便说一句。需要使用端到端测试才能正确使用TDD。要在ASP.NET MVC中编写端到端测试,您可以使用WatiN,也可以使用SpecFlow来处理BDD,但这样做确实会增加很多工作量,但是您将会真正测试应用程序。如果您想了解更多关于TDD的信息,我建议您使用this book(唯一的缺点就是Java中的例子)。

集成测试如果不使用通用存储库,并且隐藏了某些不会暴露IQueryable但直接返回数据的类中的查询,那么这种测试是有意义的。

例子:

public interface IMyEntityRepository 
{ 
    MyEntity GetById(int id); 
    MyEntity GetByName(string name); 
} 

现在你可以只写集成测试,测试执行这个仓库的,因为查询隐藏在这个类,而不是暴露到上层。但是这种类型的存储库在某种程度上被认为是与存储过程一起使用的旧实现。这种实现会失去很多ORM特性,或者你将不得不做很多额外的工作 - 例如,添加specification pattern以便能够在上层定义查询。

在ASP.NET MVC中,您可以用控制器级别的集成测试部分替换端到端测试。

编辑基于评论:

我不说,你需要的单元测试,集成测试和终端到终端的测试。我说做测试的应用程序需要更多的努力。所需测试的数量和类型取决于应用程序的复杂程度,应用程序的预期未来,其他团队成员的技能和技能。

小型直接项目可以在没有测试的情况下创建(好吧,这不是一个好主意,但我们都做到了,并且最终它可以工作),但是一旦项目通过某个阈值,您就会发现引入新功能或维护这个项目非常困难,因为你永远不知道它是否会破坏已经发挥作用的东西 - 这就是所谓的回归。防止回归的最佳防御措施是一套很好的自动化测试。

  • 单元测试可以帮助您测试方法。理想情况下,这种测试应该覆盖方法中的所有执行路径。这些测试应该非常简短并且易于编写 - 复杂的部分可以是设置依赖关系(嘲讽,伪造,存根)。
  • 集成测试可帮助您测试跨多个层的功能,并且通常跨多个进程(应用程序,数据库)进行测试。你不需要为他们提供所有的东西,而是更多地选择他们有用的地方。
  • 端到端测试类似于用例/用户故事/功能验证。他们应该涵盖整个流程的要求。

不需要多次测试feture - 如果您知道该功能是在端到端测试中测试的,则无需为同一代码编写集成测试。此外,如果您知道该方法仅具有集成测试所涉及的单一执行路径,则无需为其编写单元测试。 TDD方法的效果要好得多,您可以从一个大测试(端到端或集成)开始,然后深入单元测试。

根据您的开发方法,您不必从头开始进行多种类型的测试,但随着应用程序变得越来越复杂,您可以稍后介绍它们。 TDD/BDD是个例外,你甚至在写单行其他代码之前,应该至少开始使用端到端和单元测试。

所以你问的是错误的问题。问题不是更简单?问题是最终会对你有什么帮助,什么复杂性适合你的应用程序?如果您想要轻松地进行单元测试的应用程序和业务逻辑,您应该将EF代码封装到可以被模拟的其他类中。但在同一时间,您必须引入其他类型的测试以确保EF代码正常工作。

我不能说你用什么办法将适合你的环境/项目/团队/等,但我可以从我过去的项目解释例如:

我工作的项目约5-6个月有两个的同事。该项目基于ASP.NET MVC 2 + jQuery + EFv4,并且是以增量和迭代的方式开发的。它有很多复杂的业务逻辑和大量复杂的数据库查询。我们从通用存储库和高代码覆盖率开始,使用单元测试+集成测试来验证映射(插入,删除,更新和选择实体的简单测试)。几个月后,我们发现我们的方法不起作用。我们有超过1.200个单元测试,代码覆盖率约为60%(这不是很好)以及很多回归问题。改变EF模型中的任何内容都可能在数周内未触及的部分引入意想不到的问题。我们发现我们缺少对我们的应用程序逻辑进行的集成测试或端对端测试。关于另一个项目的平行团队也得出了同样的结论,并且使用集成测试被认为是新项目的建议。

+0

嗯。 。所以如果我理解正确的话,你说使用mock是用于业务逻辑的单元测试,我需要用真正的ef上下文以及端到端测试来进行集成测试(我将它理解为功能/用户测试。用工具li ke Watin)。但我没有看到关于建筑的观点。我很高兴你给出了有关问题的提示,但我在这个领域没有经验,所以我不知道什么是更好的解决方案。这就是我在这里寻找的。而我正在谈论“更简单”或“更简单”的意思。 – Damb 2011-04-10 13:35:55

+0

谢谢。我非常感谢你的回应和解释。我相信我现在正在使用“将ef代码包装到其他类”(我的Provider类)。只是为我的问题添加一些上下文:我正在为用户创建一个围绕任务(在项目上下文中)管理的简单应用程序(+专家系统,不会改变体系结构,因为它只是消耗数据并提供简单的输出)。这是我自己的项目(没有其他人正在研究它),我不认为它会有很好的未来。 – Damb 2011-04-10 16:29:20

+1

@Ladislav:有可能静态代码分析工具可以捕获类似于你所描述的问题(Linq to Entities不支持的Linq方法)?如果是这样,那么你可以在不必为他们编写单元测试的情况下消除一类错误,并且更加确信你所编写的测试中的模拟将“真正起作用”。它可能无法解决参考完整性问题,但正如您所说,这些可以通过集成测试(而不是E2E)来处理。 – 2011-09-19 12:05:57

13

使用存储库模式会增加复杂性吗?在你的情况下,我不这么认为。它使TDD更容易,代码更易于管理。尝试使用通用存储库模式以获得更多分离和更干净的代码。

如果您想了解更多关于实体框架TDD和设计模式,来看看:http://msdn.microsoft.com/en-us/ff714955.aspx

但是好像你正在寻找一种方法来模拟测试实体框架。一种解决方案是使用虚拟种子方法来生成数据库初始化数据。看看种子部分在:http://blogs.msdn.com/b/adonet/archive/2010/09/02/ef-feature-ctp4-dbcontext-and-databases.aspx

也可以使用一些模拟框架。最有名的,我知道有:

要查看.NET框架嘲讽的更完整列表,请访问:https://stackoverflow.com/questions/37359/what-c-mocking-framework-to-use

另一种方法是使用像SQLite这样的内存数据库提供程序。学习更多Is there an in-memory provider for Entity Framework?

最后,这里是关于单元测试实体框架的一些很好的链接(一些链接指的是Entity Framework 4.0,但你会明白的。):

http://social.msdn.microsoft.com/Forums/en/adodotnetentityframework/thread/678b5871-bec5-4640-a024-71bd4d5c77ff

http://mosesofegypt.net/post/Introducing-Entity-Framework-Unit-Testing-with-TypeMock-Isolator.aspx

What is the way to go to fake my database layer in a unit test?

+0

感谢您的输入,有一些有趣的链接在那里。但是我的问题其实并不是那么多关于测试和模拟。这更多的是寻找简单,快速,简单的架构,无需开销。您可以使用它来快速轻松地测试和开发应用程序,而无需准备XYZ代码行以查看,如果您的方法确实返回字符串值。 {Sry,有点讽刺。} – Damb 2011-04-10 04:52:34

+0

@dampe:好吧,我没有编写接口和手动模拟数据,而是提出了一些额外的解决方案,可以为你做很多工作。再一次,我会在这些情况下使用通用的存储库模式,并且从不觉得这会增加我的解决方案的复杂性。希望能帮助到你。 – Kamyar 2011-04-10 05:00:31

+0

有关通用存储库的建议,请参阅本教程:http://www.asp.net/entity-framework/tutorials/implementing-the-repository-and-unit-of-work-patterns-in-an-asp -net-mvc-application – tdykstra 2011-04-12 22:10:16

1

我有同样的问题,决定在我的MVC应用程序的通用设计。 Shiju Varghese的CodePlex项目很有帮助。它在ASP.net MVC3,EF CodeFirst中完成,并且还使用服务层和存储库层。依赖注入是使用Unity完成的。这很简单,很容易遵循。它也支持4个非常好的博客文章。它值得检查。而且,不要放弃存储库..是的。

+2

谢谢,我看了解解决方案的代码,它几乎不存在我不想要的东西......所有的存储库,IoC,工厂等等。这不是我想象的当有人说“简单的架构“:) – Damb 2011-04-10 13:27:31

+0

我可以建议的最简单的设计(虽然不推荐)是直接从您的控制器创建EF上下文对象,但正如您的问题中指出的那样,您已经尝试过,并且已经遇到问题了.. – Ben 2011-04-10 16:42:04

2

我所做的是我使用一个简单的ISession和EFSession对象,女巫很容易在我的控制器中模拟,轻松访问Linq和强类型。使用Ninject注入DI。

public interface ISession : IDisposable 
    { 
     void CommitChanges(); 
     void Delete<T>(Expression<Func<T, bool>> expression) where T : class, new(); 
     void Delete<T>(T item) where T : class, new(); 
     void DeleteAll<T>() where T : class, new(); 
     T Single<T>(Expression<Func<T, bool>> expression) where T : class, new(); 
     IQueryable<T> All<T>() where T : class, new(); 
     void Add<T>(T item) where T : class, new(); 
     void Add<T>(IEnumerable<T> items) where T : class, new(); 
     void Update<T>(T item) where T : class, new(); 
    } 

public class EFSession : ISession 
    { 
     DbContext _context; 

     public EFSession(DbContext context) 
     { 
      _context = context; 
     } 


     public void CommitChanges() 
     { 
      _context.SaveChanges(); 
     } 

     public void Delete<T>(System.Linq.Expressions.Expression<Func<T, bool>> expression) where T : class, new() 
     { 

      var query = All<T>().Where(expression); 
      foreach (var item in query) 
      { 
       Delete(item); 
      } 
     } 

     public void Delete<T>(T item) where T : class, new() 
     { 
      _context.Set<T>().Remove(item); 
     } 

     public void DeleteAll<T>() where T : class, new() 
     { 
      var query = All<T>(); 
      foreach (var item in query) 
      { 
       Delete(item); 
      } 
     } 

     public void Dispose() 
     { 
      _context.Dispose(); 
     } 

     public T Single<T>(System.Linq.Expressions.Expression<Func<T, bool>> expression) where T : class, new() 
     { 
      return All<T>().FirstOrDefault(expression); 
     } 

     public IQueryable<T> All<T>() where T : class, new() 
     { 
      return _context.Set<T>().AsQueryable<T>(); 
     } 

     public void Add<T>(T item) where T : class, new() 
     { 
      _context.Set<T>().Add(item); 
     } 

     public void Add<T>(IEnumerable<T> items) where T : class, new() 
     { 
      foreach (var item in items) 
      { 
       Add(item); 
      } 
     } 

     /// <summary> 
     /// Do not use this since we use EF4, just call CommitChanges() it does not do anything 
     /// </summary> 
     /// <typeparam name="T"></typeparam> 
     /// <param name="item"></param> 
     public void Update<T>(T item) where T : class, new() 
     { 
      //nothing needed here 
     } 

如果我想从EF4切换到让我们说MongoDB中,我只需要做的是实现一个的Isession ... MongoSession

+0

谢谢。我相信我正在做类似的事情......除了泛型和Ninject部分:) – Damb 2011-04-10 17:32:03