我有一个应用程序,其中包含使用Entity Framework 4.2 Code First和MySQL数据库处理数据的方法。我试图找出一种为这些方法编写MSTest单元测试的好方法。例如:
的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,here和here,但它们似乎都有一些优点和缺点。一组人说不应该针对EF编写单元测试,并且这属于集成测试,并且任何虚假的DbContext都不像真实的那样表现得足以达到可接受的测试目的。
我认为像这样的代码位于论证的中间位置。理想情况下,我想创建一组临时的内存中对象来表示所需的数据,而不必将其实际存储到数据库中。
如何更改上述内容并编写一组验证UserIsInRole方法的测试:
请记住,这是一个简化的示例,并且代码实际上可能包含多个任意复杂度的查询,所以我希望找到比将每个查询移动到被替换为的虚拟函数更全面的内容。测试框架返回预定义的用户记录。
答案 0 :(得分: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
编写集成测试。
答案 1 :(得分:2)
我会从你的其他域名中分离EF的知识。如果您使用DbSet,您会发现它实现了IQueryable,这足以让EF工作。创建一个定义域上下文的接口,并使您的不同具体实现(EF和Fake)实现该接口,如:
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; } }
}
可以使用以下方式:
IQueryable<User> GetUsersByManagerRole(IAppDomain domain)
{
return from u in domain.Users
where u.Role == "Manager"
select u;
}
这允许您创建一个带有任何类型的示例输入的虚假实现。接下来在单元测试中,您将创建一个新的FakeDomainContext,您可以在其中以所需的方式为单元测试设置状态。想要测试具有一定角色的用户是否可以找到?与具有一些测试角色的用户一起创建FakeDomainContext并尝试找到它们。简单干净。
答案 2 :(得分:0)
如果您不喜欢模拟DbContext,您可以模拟数据对象并将一些buiness逻辑提取到与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);
}
}
要在测试中模拟User对象,您可能需要将其属性设置为虚拟或从中提取接口。
答案 3 :(得分:0)
我个人认为像IsUserInRole这样的函数属于业务逻辑而不是数据存储,但是我经常看到的一个问题是开发人员经常将这些事情紧密地结合在一起。在类似IsUserInRole的情况下,实现通常是查找用户所属的所有角色,并查看用户是否处于指定角色。这意味着你实际上无法在没有附加某种数据库的情况下测试这一功能。
我个人认为这个问题的答案是使用存储库模式将代码与数据库的特定实现分离。这允许您模拟存储库而无需重构代码。我知道这在技术上可以通过DBContext模拟实现,但我更喜欢存储库,因为它们与特定类型的数据存储机制紧密耦合,即在本例中不依赖于EF。
这是我的博客链接,以及我个人如何解决问题。我所做的一切对我很有帮助,所以看看这个例子以及我如何使用后端数据存储到EF 4实现单元测试。
http://blog.staticvoid.co.nz/2011/10/staticvoid-repository-pattern-nuget.html
答案 4 :(得分:-1)
看看Rowan关于使用假数据库上下文进行单元测试的帖子:http://romiller.com/2012/02/14/testing-with-a-fake-dbcontext/