【问题标题】:Best practices TDD on complex objects复杂对象上的最佳实践 TDD
【发布时间】:2012-05-14 21:50:15
【问题描述】:

我正在尝试更加熟悉测试驱动开发。到目前为止,我已经看到了一些简单的示例,但在处理复杂逻辑时仍然遇到问题,例如我的 DAL 中的这种方法:

public static void UpdateUser(User user)
        {
            SqlConnection conn = new SqlConnection(ConfigurationSettings.AppSettings["WebSolutionConnectionString"]);
            SqlCommand cmd = new SqlCommand("WS_UpdateUser", conn);

            cmd.CommandType = CommandType.StoredProcedure;
            cmd.Parameters.Add("@UserID", SqlDbType.Int, 4);
            cmd.Parameters.Add("@Alias", SqlDbType.NVarChar, 100);
            cmd.Parameters.Add("@Email", SqlDbType.NVarChar, 100);
            cmd.Parameters.Add("@Password", SqlDbType.NVarChar, 50);
            cmd.Parameters.Add("@Avatar", SqlDbType.NVarChar, 50);
            cmd.Parameters[0].Value = user.UserID;
            cmd.Parameters[1].Value = user.Alias;
            cmd.Parameters[2].Value = user.Email;
            cmd.Parameters[3].Value = user.Password;
            if (user.Avatar == string.Empty)
                cmd.Parameters[4].Value = System.DBNull.Value;
            else
                cmd.Parameters[4].Value = user.Avatar;

            conn.Open();
            cmd.ExecuteNonQuery();
            conn.Close();
        }

这种方法有哪些好的 TDD 实践?

【问题讨论】:

    标签: unit-testing tdd


    【解决方案1】:

    鉴于代码已经编写好了,我们来谈谈是什么让测试变得困难。这里的主要问题是这个方法纯粹是一个副作用:它什么都不返回(void),并且它的效果在你的代码中是不可观察的,在对象领域 - 可观察到的副作用应该是在数据库中的某个地方很远,现在已经更新了一条记录。

    如果您按照“给定这些条件,当我这样做时,我应该遵守这一点”来考虑您的单元测试,您会发现您拥有的代码对于单元测试是有问题的,因为前提条件 (给定与有效数据库的连接)和后置条件(记录已更新)不能由单元测试直接访问,并且取决于该代码的运行位置(2 人在 2 台机器上“按原样”运行代码没有理由期待同样的结果)。

    这就是为什么从技术上讲,不是纯粹在内存中的测试不被视为单元测试,并且在某种程度上超出了“经典 TDD”的范围。

    在你的情况下,这里有两个想法:

    1) 集成测试。如果您想验证您的代码如何与数据库一起工作,那么您处于集成测试领域,而不是单元测试领域。 BDD 等受 TDD 启发的技术可以提供帮助。与其测试“代码单元”(通常是一种方法),不如关注整个用户或系统场景,在更高级别上进行练习。例如,在这种情况下,您可以将其置于更高的级别,并假设在您的 DAL 之上的某个地方您有名为 CreateUser、UpdateUser、ReadUser 的方法,您可能想要测试的场景类似于“假设我创建了一个用户,当我更新用户名时,然后当我读取用户时,名称应该被更新” - 然后您将针对完整的设置执行该场景,涉及数据以及 DAL 和可能的 UI。

    我发现以下MSDN article on BDD + TDD 在这方面很有趣-它很好地说明了两者如何结合在一起。

    2) 如果你想让你的方法可测试,你必须暴露一些状态。该方法的主要部分围绕构建命令展开。你可以这样概述方法:

    * grab a connection
    * create the parameters and types of the command
    * fill in the parameters of the command from the object
    * execute the command and clean up
    

    您实际上可以测试这些步骤中的大部分:可观察状态就是命令本身。你可以有一些类似的东西:

    public class UpdateUserCommandBuilder
    {
       IConnectionConfiguration config;
    
       public void BuildAndExecute(User user)
       {
          var command = BuildCommand(user);
          ExecuteCommand(command);
       }
    
       public SqlCommand BuildCommand(User user)
       {
          var connection = config.GetConnection(); // so that you can mock it
          var command = new SqlCommand(...)
    
          command = CreateArguments(command); // you can verify that method now
          command = FillArguments(command, user); // and this one too
    
          return command;
       }
    }
    

    我不会一直讲到这里,但我认为大纲传达了这个想法。走这条路将有助于使构建器的步骤可验证:您可以断言是否创建了正确的命令。这有一些价值,但仍然不会告诉您命令执行是否成功,因此值得考虑这是否值得使用您的测试预算!可以说,执行整个 DAL 的更高级别的集成测试可能更经济。

    希望这会有所帮助!

    【讨论】:

    • 难道不能通过在单元测试开始时启动事务并在最后回滚来实现这一点吗?这样我们就可以测试它对数据库所做的一切,并且没有一个会被持久化。
    【解决方案2】:

    我会将方法声明更改为:

    public static void UpdateUser(User user, SqlConnection conn);

    然后您可以传入配置的 SQL 连接。在实际应用程序中,您依赖于 AppSettings 告诉您所需连接的信息,但在您的测试中,您给它一个假连接,只允许您记录针对该连接执行的命令。然后,您可以验证该方法是否正确请求存储的查询并发送正确的参数作为结果。

    【讨论】:

      猜你喜欢
      • 2017-09-21
      • 2020-07-19
      • 1970-01-01
      • 2023-03-19
      • 1970-01-01
      • 1970-01-01
      • 2012-05-28
      • 2021-03-13
      • 1970-01-01
      相关资源
      最近更新 更多