我已经阅读了那么多(几十篇帖子)关于一件事:
如何对包含实体框架代码的业务逻辑代码进行单元测试。
我有一个包含3层的WCF服务:
我的业务逻辑使用 DbContext 进行所有数据库操作。 我的所有实体现在都是POCO(曾经是ObjectContext,但我改变了它。)
我已经阅读 Ladislav Mrnka的回复here和here了解我们应该不模仿\假冒 DbContext <的原因/ em>的
他说: “这就是为什么我认为处理上下文/ Linq到实体的代码应该用集成测试覆盖并且与真实数据库一起工作的原因。”和: “当然,您的方法在某些情况下有效,但单元测试策略必须适用于所有情况 - 为了使其工作,您必须完全从测试方法中移动EF和IQueryable。”
我的问题是 - 你是如何实现这一目标的?
public class TaskManager
{
public void UpdateTaskStatus(
Guid loggedInUserId,
Guid clientId,
Guid taskId,
Guid chosenOptionId,
Boolean isTaskCompleted,
String notes,
Byte[] rowVersion
)
{
using (TransactionScope ts = new TransactionScope())
{
using (CloseDBEntities entities = new CloseDBEntities())
{
User currentUser = entities.Users.SingleOrDefault(us => us.Id == loggedInUserId);
if (currentUser == null)
throw new Exception("Logged user does not exist in the system.");
// Locate the task that is attached to this client
ClientTaskStatus taskStatus = entities.ClientTaskStatuses.SingleOrDefault(p => p.TaskId == taskId && p.Visit.ClientId == clientId);
if (taskStatus == null)
throw new Exception("Could not find this task for the client in the database.");
if (taskStatus.Visit.CustomerRepId.HasValue == false)
throw new Exception("No customer rep is assigned to the client yet.");
TaskOption option = entities.TaskOptions.SingleOrDefault(op => op.Id == optionId);
if (option == null)
throw new Exception("The chosen option was not found in the database.");
if (taskStatus.RowVersion != rowVersion)
throw new Exception("The task was updated by someone else. Please refresh the information and try again.");
taskStatus.ChosenOptionId = optionId;
taskStatus.IsCompleted = isTaskCompleted;
taskStatus.Notes = notes;
// Save changes to database
entities.SaveChanges();
}
// Complete the transaction scope
ts.Complete();
}
}
}
在附带的代码中,我的业务逻辑演示了一个函数。 该函数有几次“跳转”到数据库。 我不明白我怎么能从这个函数中删除 EF代码到一个单独的程序集,这样我就可以单元测试这个函数了(通过注入一些假数据而不是EF数据),集成测试包含'EF函数'的程序集。
Ladislav或其他任何人可以帮忙吗?
[编辑]
以下是我的业务逻辑中的另一个代码示例,我不明白如何从我测试的方法中移出EF和IQueryable代码:
public List<UserDto> GetUsersByFilters(
String ssn,
List<Guid> orderIds,
List<MaritalStatusEnum> maritalStatuses,
String name,
int age
)
{
using (MyProjEntities entities = new MyProjEntities())
{
IQueryable<User> users = entities.Users;
// Filter By SSN (check if the user's ssn matches)
if (String.IsNullOrEmusy(ssn) == false)
users = users.Where(us => us.SSN == ssn);
// Filter By Orders (check fi the user has all the orders in the list)
if (orderIds != null)
users = users.Where(us => UserContainsAllOrders(us, orderIds));
// Filter By Marital Status (check if the user has a marital status that is in the filter list)
if (maritalStatuses != null)
users = users.Where(pt => maritalStatuses.Contains((MaritalStatusEnum)us.MaritalStatus));
// Filter By Name (check if the user's name matches)
if (String.IsNullOrEmusy(name) == false)
users = users.Where(us => us.name == name);
// Filter By Age (check if the user's age matches)
if (age > 0)
users = users.Where(us => us.Age == age);
return users.ToList();
}
}
private Boolean UserContainsAllOrders(User user, List<Guid> orderIds)
{
return orderIds.All(orderId => user.Orders.Any(order => order.Id == orderId));
}
答案 0 :(得分:5)
如果您想单元测试您的TaskManager
类,您应该使用存储库dessign模式并将存储库(如UserRepository或ClientTaskStatusRepository)注入此类。然后,您将使用这些存储库并调用其方法,而不是构建CloseDBEntities
对象,例如:
User currentUser = userRepository.GetUser(loggedInUserId);
ClientTaskStatus taskStatus =
clientTaskStatusRepository.GetTaskStatus(taskId, clientId);
如果您想要集成测试您的TaskManager
课程,解决方案会更加简单。您只需要使用指向测试数据库的连接字符串初始化CloseDBEntities
对象即可。实现此目的的一种方法是将CloseDBEntities
对象注入TaskManager
类。
您还需要在每次集成测试运行之前重新创建测试数据库,并使用一些测试数据填充它。这可以使用Database Initializer来实现。
答案 1 :(得分:4)
这里有几个误解。
首先:存储库模式。对于单元测试,它不仅仅是DbSet的外观!存储库是与域驱动设计的聚合和 Aggoundate Root 概念密切相关的模板。聚合是一组应该保持彼此一致的相关实体。我的意思是业务一致性,而不仅仅是外键有效性。例如:订购了2个订单的客户应获得5%的折扣。因此,我们应该以某种方式管理与客户实体相关的订单实体数量与客户实体的折扣属性之间的一致性。负责此的节点是聚合根。它也是唯一可以直接从聚合外部访问的节点。存储库是一个从一些(可能是持久的)存储中获取聚合根的实用程序。
一个典型的用例是创建UoW / Transaction / DbContext / WhateverYouNameIt,从存储库中获取一个聚合根实体,在其上调用一些方法或通过遍历根访问其他一些实体,提交/保存更改/随便。看,它与你的样品有多远。
第二:业务逻辑。我已经向您展示了一个例子:订购了2个订单的客户应该获得5%的折扣。相反:您的第二个代码示例不是业务逻辑。这是只是一个查询。此代码的责任是从存储中获取一些数据。在这种情况下,它背后的存储技术确实很重要。所以我建议在这里推荐集成测试而不是假装存储与存储交互是无关紧要的。
我还会将其封装在已经建议的查询对象中。然后 - 这样的查询对象可以被模拟。不仅仅是背后的DbContext。整个QO。
第一个代码示例更好一些,因为它可能会解决一些业务逻辑,但这很难识别。这引出了我们第三个问题。
第三:贫血领域模型。您的域名看起来并非面向对象。你有一些愚蠢的实体和交易脚本。有7个参数!那是纯粹的程序编程。
此外,在您的UpdateTaskStatus用例中 - 聚合根是什么?请你回答一下,最重要的问题是:你究竟想做什么?是......嗯......标记用户访问时当前完成的任务?也许,客户实体内部应该有一个方法Visit()?这个方法应该有这样的东西.CurrentTaskStatus.IsCompleted = true? 这只是一个随机的猜测。如果我错过了,这显然会显示另一个问题。域模型应该使用无处不在的语言 - 这对程序员和企业来说是常见的。您的代码没有共同语言赋予的那种表达能力。我只是不知道UpdateTaskStatus中有7个参数发生了什么。
如果您在实体中执行正确的表达方法来执行业务操作,那么也会强制您不要在那里使用DbContext,因为您需要实体保持持久性无知。那么嘲弄的问题就消失了。您可以在没有持久性问题的情况下测试纯业务逻辑。
所以最后一句话:首先重新考虑你的模型。首先使用无处不在的语言使您的API具有表现力。
PS:请不要把我视为权威。我可能完全错了,因为我刚开始学习DDD。