一个高效的软件开发过程对软件开发人员来说是至关重要的,决定着开发是痛苦的挣扎,还是不断进步的喜悦。国人对软件蓝领的不屑,对繁琐冗长的传统开发过程的不耐,使大多数开发人员无所适从,而测试驱动开发(Test-Driven Development)就是为了改善传统的以实现为目标的软件开发流程,利用测试来驱动软件程序的设计和实现,从测试的角度提出的一种全新的开发方式。测试驱动开发可以有效的避免过度设计带来的浪费,同时也可以让开发者在开发中拥有更全面的视角,避免过度实现带来的浪费。因此,测试驱动开发成为极限编程中比较流行的一种开发方式,受到很多开发者的青睐。

  测试驱动开发的整个过程跟传统的软件开发过程有很大的区别,它的基本过程如下:

  1) 明确当前要完成的功能。可以记录成一个 TODO 列表。

  2) 快速完成针对此功能的测试用例编写。

  3) 测试代码编译不通过。

  4) 编写对应的功能代码。

  5) 测试通过。

  6) 对代码进行重构,并保证测试通过。

  7) 循环完成所有功能的开发。

  为了保证这一过程能够快捷方便地进行,通常我们会采用很多开发工具来支持这一过程。在应用最为广泛的开发工具Visual Studio中,因为有.NET Framework的支持,我们可以很轻松方便地进行C#和Visual Basic语言的测试,所以使用这两种语言实践测试驱动开发也很方便。但是,作为Visual Studio中最重要的开发语言的C++,在以往的Visual Studio的版本中也没法方便地进行测试。如果要实践面向C++语言的测试驱动开发,我们不得不借助第三方测试工具,比如CPPUnit来帮助进行测试。在整个过程中,我们要使用CPPUnit进行测试,而开发又是在Visual Studio中进行,两个工具的衔接协作,给测试驱动开发带来了很多不便。使用测试驱动开发流程的开发人员热切地盼望有一个面向C++的开发工具可以把测试驱动开发过程中最重要的两个过程:“测试”和“开发”结合起来,两者能够做到无缝衔接,让测试真正地驱动开发。

  幸运的是,开发人员的这一梦想在Visual Studio 2010中成为了现实。全新的Visual Studio 2010已经不仅仅是一个开发工具,它也集成了大量的测试工具,成为一个完整的开发平台。在Visual Studio 2010中,我们可以创建面向C++的测试项目来完成测试驱动开发流程中的测试环节,从而让整个测试驱动开发过程都在Visual Studio中进行,让测试和开发做到了无缝衔接,而测试也真正地驱动了开发。

  光说不练假把式。为了让大家更加了解如何在Visual Studio中进行面向C++的测试驱动开发,我们来看一个实际的例子,在例子中体会这一过程是多么简单方便。例如,我们要编写一个计算工资的类Salary,它可以根据员工的入职年份和现在的年份计算整个员工应该得到的工资。

  按照测试驱动开发的过程,我们首先设计完成这个Salary类需要实现的功能,为了简便,我们让这个类只需要完成两个简单的功能:

  1) 能够给定员工的入职年份,并根据现在的年份给出应得的工资

  2) 能够对错误的输入年份返回相应的错误代码

  既然是测试驱动开发,当然是“开发未动,测试先行”了。按照下面的步骤,首先创建一个测试项目并编写测试对设计中的功能进行测试:

  1) 启动Visual Studio 2010并创建一个新的“Visual Studio空白解决方案”,方案名字叫做SalarySys。接下来的所有测试和开发都会在这个解决方案中进行。

  2) 向刚刚创建的解决方案中添加一个Visual C++测试项目SalaryTest。因为我们的测试需要使用C++/CLI进行编写以便使用.NET的单元测试框架,所以我们同时要修改测试项目属性,让它使用公共语言运行时(/clr)支持。

  3) 向测试项目SalaryTest中添加一个单元测试。默认情况下,Visual Studio会为我们创建一个UnitTest1.cpp文件,在其中我们就可以编写针对将要实现的工资计算类测试了。

  4) 在UnitTest1.cpp文件中找到“#pragma region Additional test attributes”,在这个区域中,我们编写一个测试来对Salary的基本功能进行测试。

  // 创建测试类的智能指针

  // 测试功能设计中的“能够给定员工的入职年份”

  std::unique_ptr pClass(new Salary(2003));

  // 测试功能设计中的“根据现在的年份给出应得的工资”

  // 判断函数返回结果是否符合预期

  Assert::AreEqual(1900, pClass->GetSalary(2006));

  这里我们首先创建了一个Salary类的实例智能指针,其中构造函数的参数2003表示入职年份,然后调用其GetSalary()函数计算工资,其参数2006表示现在的年份。按照设计的计算规则,其结果应该是1300,这里我们使用Assert::AreEqual函数对测试结果进行判断,如果这个断言函数通过,则表示这个Salary类的测试通过。

  除了使用Assert::AreEqual断言函数对结果进行判断之外,Visual C++还提供了多种断言函数,以满足对不同类型的返回结果进行判断的需要。更人性化的是,我们还可以在断言函数中添加对测试结果的说明,这样我们更容易以测试的结果来驱动开发。例如:

  // 判断不相等

  Assert::AreNotEqual(0, (DWORD_PTR) pClass, "pClass指针不应该为空指针");

  // 判断相等

  Assert::AreEqual(0, (DWORD_PTR) pClass, "pClass指针应该为空指针");

  // 判断比较结果是否为true

  Assert::IsTrue(pClass == nullptr, "pClass指针应该为空指针");

  // 判断StringValue()返回的字符串是否与期望的结果相等

  Assert::AreEqual("期望的结果", gcnew String(pClass->StringValue());

  如果我们现在直接运行这个测试项目,当然是没法编译通过的。因为我们还没有创建Salay类。接下来就是测试驱动开发中的“开发”部分了。为了让刚才创建的测试项目能够编译运行并通过所有的测试,我们需要做的是:

  1) 向SalarySys解决方案中新添加一个Visual C++的Win 32静态库项目Salary。我们的开发工作就在这个项目中进行。

  2) 在Salay项目中新添加一个类Salary以实现工资计算的功能。为了让测试项目中的测试可以通过,我们先将Salay类简单地实现如下:

  
  class Salary

  {

  public:

  Salary(int nBaseYear) {};

  public:

  int GetSalary(int nNow)

  {

  return 1300;

  }

  };


  3) 有了开发项目之后,接下来的工作就是将开发项目和测试项目联系起来,让测试项目对开发项目进行测试。首先,将开发项目目录“$(SolutionDir)\Salary\”添加为测试项目的附加包含目录目录,这样测试项目可以找到Salary类的定义。然后我们还要将开发项目这个静态库链接到测试项目,为了完成这一步,我们需要在项目属性中将解决方案的输出目录“$(OutputPath)”设置为测试项目的附加库目录,然后将静态库Salary.lib设置为测试项目的附加依赖项。

  完成这些开发项目的编写以及测试项目的设置之后,我们的测试项目就可以编译通过并运行其中的测试了。

  到这里,我们就完成了测试驱动开发过程的一次完整的迭代,接下来的工作,就是编写更多的测试以覆盖设计中的所有用例,然后运行这些测试使这些测试通过,如果测试暂时无法通过,则对开发代码进行重构实现设计使得所有测试都可以通过。例如,在上面完成第一次开发迭代的基础,我们可以编写更多的测试来覆盖Salary类设计中的所有功能点:

  
  [TestMethod]

  void TestCalculationn()

  {

  std::unique_ptr pClass(new Salary(2003));

  int cases[4][2] =

  {{2003,1000},{2004,1300},

  {2005,1600},{2011,3100}

  };

  for(int i = 0; i < 4; ++i)

  {

  Assert::AreEqual(cases[i][1], pClass->GetSalary( cases[i][0]));

  }

  };


  在这里我们使用一个数组cases定义了多个测试用例,然后使用for循环对这些用例逐个进行测试。现在我们运行测试项目中的这些测试当然是没法通过的,所以这些测试就驱动我们去对开发项目中的Salary类进行重构,以使得这些测试可以通过:

  
  class Salary

  {

  public:

  Salary(int nBaseYear)

  :m_nBaseYear(nBaseYear)

  {};

  public:

  int GetSalary(int nNow)

  {

  return 300*(nNow - m_nBaseYear) + 1000;

  }

  private:

  int m_nBaseYear;

  };


  对Salary类进行重构之后,所有测试都可以通过了,这样也就实现了用测试来驱动开发。当然,我们这里只是对设计中的第一个功能进行了足够的测试,完成了第一个功能的开发。我们还可以在这个基础上编写更多的测试,进入下一个以测试驱动开发的迭代。我们可以按照上面的过程以测试来驱动第二个功能的实现:

  
  [TestMethod]

  void TestInvalidInput()

  {

  std::unique_ptr pClass(new Salary(2003));

  // 测试第二个功能点:能够对错误的输入年份返回相应的错误代码

  Assert::AreEqual(-1, pClass->GetSalary(2001));

  // 测试临界输入是否返回正确结果

  Assert::AreEqual(1000, pClass->GetSalary(2003));

  };


  为了让这个测试通过,我们必须对Salary进行重构,让它对错误的输入进行处理并返回相应的错误代码:

  
  class Salary

  {

  public:

  Salary(int nBaseYear)

  :m_nBaseYear(nBaseYear)

  {};

  public:

  int GetSalary(int nNow)

  {

  int nYears = nNow - m_nBaseYear;

  // 对错误的输入进行处理并返回相应的错误代码

  {

  return -1;

  }

  else

  {

  // 正确的输入返回相应的计算结果

  return 300*(nNow - m_nBaseYear) + 1000;

  }

  }

  private:

  int m_nBaseYear;

  };


  经过重构之后,测试项目中的所有测试都可以顺利通过,这也宣告了我们这一次的代代已经顺利完成。而通过不断地进行这样的迭代,我们可以实现设计中的所有功能。

  经过简单的几次迭代,我们就以测试驱动了Salary的实现。整个过程完美地体现了测试驱动开发的强大优势:这种从使用者角度对代码进行的设计通常更符合后期开发的需求。因为关注用户反馈,可以及时响应需求变更,同时因为从使用者角度出发的简单设计,也可以更快地适应变化。同时,测试驱动开发将测试工作提到编码之前,并频繁地运行所有测试,可以尽量地避免和尽早地发现错误,极大地降低了后续测试及修复的成本,提高了代码的质量。在测试的保护下,不断重构代码,以消除重复设计,优化设计结构,提高了代码的重用性,从而提高了软件产品的质量。

  测试驱动开发的优势自不待言,从整个过程中我们也可以体会到,有了Visual C++ 2010的支持,面向C++的测试驱动开发也可以同样的快捷而简便。测试驱动开发的优势如此诱人,而我们又有了Visual C++ 2010这个工具的强力支持,我们不妨一试,让测试驱动开发使得我们的生活更加轻松。