2

关于一件事,我已经阅读了很多(数十篇帖子):

如何对包含实体框架代码的业务逻辑代码进行单元测试。

我有一个 3 层的 WCF 服务:

  • 服务层
  • 业务逻辑层
  • 数据访问层

我的业务逻辑将DbContext用于所有数据库操作。我所有的实体现在都是 POCO(以前是 ObjectContext,但我改变了它)。

我已经在这里这里阅读了Ladislav Mrnka 的回答, 了解我们不应该模拟\ 伪造DbContext的原因。

他说: “这就是为什么我认为处理上下文/Linq-to-entities 的代码应该包含在集成测试中并针对真实数据库工作的原因。”

并且: “当然,您的方法在某些情况下有效,但单元测试策略必须在所有情况下都有效 - 要使其有效,您必须将 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 函数”。

拉迪斯拉夫或其他人可以帮忙吗?

[编辑]

这是我的业务逻辑中的另一个代码示例,我不明白如何从我的测试方法中“移动 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));
}
4

2 回答 2

5

如果你想对你的类进行单元测试TaskManager,你应该使用存储库设计模式并将诸如 UserRepository 或 ClientTaskStatusRepository 的存储库注入到这个类中。然后,您将使用这些存储库并调用它们的方法,而不是构造CloseDBEntities对象,例如:

User currentUser = userRepository.GetUser(loggedInUserId);
ClientTaskStatus taskStatus = 
    clientTaskStatusRepository.GetTaskStatus(taskId, clientId);

如果你想对你的类进行集成测试TaskManager,解决方案要简单得多。您只需要CloseDBEntities使用指向测试数据库的连接字符串初始化对象即可。如何实现这一点的一种方法是将CloseDBEntities对象注入到TaskManager类中。

您还需要在每次集成测试运行之前重新创建测试数据库,并用一些测试数据填充它。这可以使用Database Initializer来实现。

于 2012-06-08T10:08:18.903 回答
4

这里有几个误解。

第一:存储库模式。它不仅仅是用于单元测试的 DbSet 的外观!该存储库是与域驱动设计的聚合聚合根概念密切相关的模式. 聚合是一组相互保持一致的相关实体。我的意思是业务一致性,而不仅仅是外键有效性。例如:一个客户下了 2 个订单,应该得到 5% 的折扣。因此,我们应该以某种方式管理与客户实体相关的订单实体数量与客户实体的折扣属性之间的一致性。对此负责的节点是聚合根。它也是唯一可以从聚合外部直接访问的节点。存储库是一个实用程序,用于从某些(可能是持久的)存储中获取聚合根。

一个典型的用例是创建一个 UoW/Transaction/DbContext/WhateverYouNameIt,从存储库中获取一个聚合根实体,在其上调用一些方法或通过从根遍历 Commit/SaveChanges/Whatever 访问其他一些实体。看,它与你的样品有多大不同。

第二:业务逻辑。我已经向您展示了一个示例:一位客户下了 2 个订单,应该获得 5% 的折扣。相反:您的第二个代码示例不是业务逻辑。这只是一个查询。这段代码的职责是从存储中获取一些数据。在这种情况下,其背后的存储技术确实很重要。所以我会在这里推荐集成测试,而不是在与存储交互时假装存储无关紧要是这个功能的唯一目的。

我还将把它封装在已经建议的查询对象中。然后 - 可以模拟这样的查询对象。不仅仅是它背后的 DbContext。整个QO。

第一个代码示例要好一些,因为它可能涉及一些业务逻辑,但这很难识别。这将我们引向第三个问题。

第三:贫血领域模型。您的域看起来不太面向对象。你有一些愚蠢的实体和交易脚本。有7个参数!那是纯粹的过程编程。

此外,在您的 UpdateTaskStatus 用例中 - 什么是聚合根?在你回答这个问题之前,首先是最重要的问题:你到底想做什么?那是……嗯……标记用户在被访问时完成的当前任务?比,也许客户实体中应该有一个方法 Visit() ?这个方法应该有这样的东西。CurrentTaskStatus.IsCompleted = true?那只是一个随机的猜测。如果我错过了,那将清楚地表明另一个问题。领域模型应该使用普遍存在的语言——对于程序员和企业来说是通用的。您的代码没有通用语言所赋予的那种表达能力。我只是不知道带有 7 个参数的 UpdateTaskStatus 中发生了什么。

如果您在实体中放置适当的表达方法来执行业务操作,这也将强制您根本不在那里使用 DbContext,因为您需要您的实体保持对持久性的无知。然后嘲笑的问题就消失了。您可以测试纯业务逻辑而无需担心持久性。

所以最后一句话:首先重新考虑你的模型。首先使用通用语言使您的 API 富有表现力。

PS:请不要把我当成权威。我可能完全错了,因为我刚刚开始学习 DDD。

于 2012-06-18T16:24:52.003 回答