2012-02-20 32 views
2

我有一个应用程序,其中包含使用Entity Framework 4.2 Code First和MySQL数据库处理数据的方法。我正在尝试找出为这些方法编写MSTest单元测试的好方法。例如:如何为使用实体框架4.2的代码编写单元测试?

的DataModel:

public class User 
{ 
    public User() { } 
    [Key] 
    public int UserID { get; set; } 
    public string Role { get; set; } 
} 

public class AppDbContext : DbContext 
{ 
    public DbSet<User> Users { get; set; } 
} 

业务层:

public class Bus 
{ 
    public bool UserIsInRole(int userID, string role) 
    { 
     using(var context = new AppDbContext()) 
     { 
      User user = context.Users.SingleOrDefault(p => p.UserID == userID); 
      if (user == null) 
       return false; 
      return user.Roles.Split(',').Contains(role); 
     } 
    } 
} 

我想写一组单元测试的UserIsInRole功能,但我想尝试自己从隔离实际上必须读取和写入实际的数据库,因为我无法在测试之前保证其状态。仅仅为这个测试设置/拆除数据库将需要很长时间。

我遇到过很多关于使用假DbContext的文章,比如here,herehere,但它们都似乎有一些优点和缺点。一群人说,不应该针对EF编写单元测试,并且这属于集成测试,并且任何假的DbContext都不能像可接受测试的目的那样表现得足够真实。

我认为这样的代码位于参数中间的某个位置。理想情况下,我想创建一组表示所需数据的临时内存对象,而无需将其实际存储到数据库中。

你将如何更改上面写的一组验证UserIsInRole方法测试:

  1. 返回false如果用户ID中不存在的用户收集 。
  2. 如果用户不包含 所需的角色,则返回false。
  3. 如果用户具有所需的角色,则返回true。

请记住,这是一个简单的例子,该代码实际上可以包含任意复杂的多个查询,所以我希望能找到的东西有点比更全面,比如说,每个查询移动到这是一个虚函数由测试框架替换以返回预定义的用户记录。

回答

2

我会从您的域的其余部分分离知识的EF。如果你检查DbSet,你会发现它实现了IQueryable,这足以让EF工作。创建一个定义域范围内的接口,使您的不同具体实现(EF和假)实现该接口,如:

IQueryable<User> GetUsersByManagerRole(IAppDomain domain) 
{ 
    return from u in domain.Users 
      where u.Role == "Manager" 
      select u; 
} 

这使您可以:

public class User 
{ 
    public User() { } 
    [Key] 
    public int UserID { get; set; } 
    public string Role { get; set; } 
} 

public interface IAppDomain 
{ 
    public IQueryable<User> Users { get; } 
} 

public class AppDbContext : DbContext, IAppDomain 
{ 
    // exposure for EF 
    public DbSet<User> Users { get; set; } 

    IAppDomain.IQueryable<User> Users { get { return ((AppDbContext)this).Users; } 
} 

public class FakeAppDomain : IAppDomain 
{ 
    private List<User> _sampleUsers = new List<User>(){ 
     new User() { UserID = 1, Role = "test" } 
    } 

    public IQueryable<User> Users { get { return _sampleUsers; } } 
} 

这可以像使用方式创建一个可以接受任何类型样本输入的假实现。接下来在你的单元测试中,你创建了一个新的FakeDomainContext,你可以用你想要的单元测试方式设置状态。想要测试具有一定作用的用户可以找到吗?用具有某些测试角色的用户创建一个FakeDomainContext并尝试查找它们。简单而干净。

+0

这是我正在考虑的事情。它将允许任何代码从数据源创建所需的任何查询。我担心的是,查询可能与在Linq到Entities之间返回的数据以及Linq到对象的(我认为)有什么不同。您对结果查询兼容性有任何经验吗?例如,编译良好并且通过测试但不能抵抗真实数据(区分大小写,不受支持的函数,空行为,结果分组)的情况。显然,无论如何,都需要一定程度的集成测试。 – 2012-02-22 20:19:59

+0

我只是在我的代码中尝试了这种方法。但是,我使用的功能在IQueryable <>中不受支持,例如Create(),Add(),Remove()等。它看起来像我还需要为每个实体创建某种接口,或者将这些移动到IAppDomain接口。 – 2012-02-22 21:07:49

+0

@DanC - 你也可以公开IDbSet而不是IQueryable。这给你所有他的选择,但允许你创建一个虚假的实现 – Polity 2012-02-23 02:07:28

0

如果你不喜欢嘲笑的DbContext,你可以嘲笑的数据对象,并提取了一些理财周报的逻辑为DB-独立的可测试方法。

public class Bus 
{ 
    private UnitTestable impl; 

    public bool UserIsInRole(int userID, string role) 
    { 
     using(var context = new AppDbContext()) 
     { 
      return impl.UserInRole(context.Users, role); 
     } 
    } 
} 

public class UnitTestable 
{ 
    public bool UserInRole(IQueryable<User> users, string role) 
    { 
     User user = users.SingleOrDefault(p => p.UserID == userID); 
     if (user == null) 
      return false; 
     return user.Roles.Split(',').Contains(role); 
    } 
} 

要嘲笑测试中的用户对象,您可能需要使其属性变为虚拟或从中提取一个接口。

0

我个人认为像IsUserInRole这样的函数属于业务逻辑而不是数据存储,但是我看到很多问题之一是开发人员经常将这些东西紧紧地耦合在一起。在类似IsUserInRole的情况下,实现通常是查找用户所属的所有角色,并查看用户是否处于指定角色。这意味着,如果没有附加某种数据库,您实际上无法测试这部分功能。

我个人认为这个问题的答案是使用存储库模式从数据库的特定实现中解耦代码。这使您可以模拟存储库而无需重构代码。我知道这在技术上有可能通过DBContext模拟来实现,但我更喜欢存储库,因为它们与特定类型的数据存储机制紧密耦合,即在此示例中不与EF绑定。

下面是我的博客链接,以及我个人如何解决问题。我所做的事情一直担任我很好所以来看看的例子,我是如何实现的单元测试与后端数据存储到EF 4

http://blog.staticvoid.co.nz/2011/10/staticvoid-repository-pattern-nuget.html

3

你的代码是不是测试。如果您在被测系统中直接使用new,您如何伪造某些东西?

提高代码:

public class Bus 
{ 
    public bool UserIsInRole(int userID, string role) 
    { 
     using(var context = CreateContext()) 
     { 
      User user = ExecuteGetUserQuery(context, userId); 
      if (user == null) 
       return false; 
      return user.Roles.Split(',').Contains(role); 
     } 
    } 

    protected virtual IAppDbContext CreateContext() 
    { 
     return new AppDbContext(); 
    } 

    protected virtual User ExecuteGetUserQuery(IAppDbContext context, int userId) 
    { 
     return context.Users.SingleOrDefault(p => p.UserID == userID); 
    } 
} 

现在不引入任何新的类(只是单一的界面为你的背景下),我们使你的代码可测试:

  • 首先我们所做的是将单一职责原则并将您的方法分为三个单独的职责:
    • 上下文创建
    • 查询执行
    • UserIsInRole逻辑
  • 我们增加了界面背景,让您的逻辑依赖于抽象而不是实现
  • 我们还增加了一些挂钩于测试

替换实施当你想编写单元测试UserIsInRole(和其他纯粹的单元测试),您可以使测试派生实现Bus类,并返回来自ExecuteGetUserQuery的替代版本的任何假数据。通过覆盖CreateContext,您还将使您的测试完全独立于数据库或EF。重写这些方法不会导致测试不同的逻辑,因为无论如何你都会伪造这些数据。测试的UserIsInRole方法在派生类中未被修改。

当然,不是提供虚拟方法,您可以将此功能移入单独的一个或多个类中,并使用存根或嘲笑,但对于简单场景这可行。如果您需要测试与数据库的交互,则只会编写集成测试ExecuteGetUserQuery

+0

谢谢拉迪斯拉夫。我实际上希望找到避免将每个查询移动到其自己的虚拟函数的解决方案,因为可以有许多不同的查询,连接数据,过滤等等,并且我希望在数据库中做同样的事情,而不必返回整个数据集。 (我意识到你的例子是单元测试重构101.我的生产代码确实做了类似的事情,但我希望有更多的高级EF特定的替代品)。 – 2012-02-22 20:04:40

+1

EF特定的单元测试替代[不存在](http://stackoverflow.com/questions/6904139/fake-dbcontext-of-entity-framework-4-1-to-test/6904479#6904479)。人们以某种​​方式寻找测试EF代码的方式,而不考虑他们将要测试的方法。正确的单元测试要求你的测试代码只做一件事,所以如果你想正确地做到这一点,你最终会用不同的方法进行查询。否则,您的测试将会过于复杂且难以维护。 – 2012-02-22 20:17:57

相关问题