Unit testing 复杂对象上的最佳实践TDD

Unit testing 复杂对象上的最佳实践TDD,unit-testing,tdd,Unit Testing,Tdd,我正试图更加熟悉测试驱动的开发。到目前为止,我已经看到了一些简单的示例,但在处理复杂逻辑时仍然存在一些问题,例如我的DAL中的这种方法: public static void UpdateUser(User user) { SqlConnection conn = new SqlConnection(ConfigurationSettings.AppSettings["WebSolutionConnectionString"]);

我正试图更加熟悉测试驱动的开发。到目前为止,我已经看到了一些简单的示例,但在处理复杂逻辑时仍然存在一些问题,例如我的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实践

既然代码已经编写好了,那么让我们来谈谈是什么让测试变得困难。这里的主要问题是,这个方法纯粹是一个副作用:它不返回任何内容(void),并且它的效果在您的代码中是不可见的,在objectland中-可见的副作用应该是,在很远的数据库中,某个记录现在已经被更新了

如果您认为您的单元测试是“给定这些条件,当我这样做时,那么我应该遵守这一点”,那么您可以看到您的代码对于单元测试来说是有问题的,因为前置条件(给定与有效DB的连接)和后置条件(记录已更新)不能直接由单元测试访问,并且取决于代码运行的位置(在两台机器上“按原样”运行代码的两个人没有理由期望相同的结果)

这就是为什么从技术上讲,不完全在内存中的测试不被认为是单元测试,并且有点超出了“经典TDD”的范围

在您的情况下,以下是两种想法:

1) 集成测试。如果您想验证代码如何与数据库一起工作,那么您所处的领域是集成测试,而不是单元测试。受TDD启发的技术(如BDD)可以有所帮助。与其测试“代码单元”(通常是一种方法),不如将重点放在整个用户或系统场景上,在更高的层次上进行测试。例如,在本例中,您可以在更高的级别上使用它,并假设在DAL的顶部有名为CreateUser、UpdateUser、ReadUser的方法,您可能希望测试的场景类似于“给定我创建了一个用户,当我更新用户名时,当我读取用户名时,应该更新用户名”-然后,您将针对完整的设置演练场景,包括数据、DAL和可能的UI

从这一点上,我发现下面的内容很有趣——它很好地说明了这两个元素是如何相互啮合的

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的更高级别集成测试可能更经济


希望这有帮助

我会将方法声明更改为:

公共静态void UpdateUser(用户用户,SqlConnection conn)


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

在单元测试开始时启动一个事务,然后在结束时将其回滚,难道不能实现这一点吗?通过这种方式,我们可以测试它对数据库所做的一切,但没有一个会被持久化。