[原文:Test Driven Development with Visual Studio 2005 Team System]
[中文名:Visual Studio 2005 Team System的测试驱动开发]
[出处:http://www.dotnetjunkies.com/]
[作者:Doug Seven]
[翻译:极地银狐.NET]
测试驱动开发(TDD)也不是新概念了。实际上先写测试代码再写功能代码这种情况已经出来有好几年了。最近Microsoft发布的主流开发工具,Visual Studio 2005 Team System增加了很多新的特性,其中就包括软件测试。这对于你,严谨的开发人员来说意味着什么呢?意味着你可以用单元测试来做TDD开发。
什么是测试驱动开发?
我个人喜欢把TDD定义成为先用程序员能理解的编程语言来组织需求文档,再写代码来实现这些需求。在 Test-Driven Development: By Example (Addison-Wesley)这本书中Kent Beck把TDD定义为以下两个简单的规则:
<!--[if !supportLists]-->1, <!--[endif]-->除非你在自动测试中失败,否则一行代码也不要写。
<!--[if !supportLists]-->2, <!--[endif]-->消除重复(代码)。
Kent的规则和我对TDD的定义不谋而合。在一般的软件开发过程中,项目负责人或是项目经理会提供一堆需求;典型的这些需求都是文字文档存在并冠以“Functional Specification”这类标题;诚然这些文字文档对于描述故事还是很不错的,但我们真正要的是把这些被项目经理们讲述的故事翻译成编程语言,把这些当作用C#(或Visual Basic)描述的需求文档来对待。这些需求作为测试编写成(测试)代码;每个测试都代表了一个需求。另外,在我们准备写功能代码的时候(编写少许),要确保在一个以上使用相同代码的地方被集中到一起
以消除多余的代码。
测试类型
每次我同人们说起软件测试的时候大家好像总马上想到做一个测试确保程序是否按预期的运行。这点当然重要,而且(这种测试)可以理解为两方面:
程序员测试:程序员写的自动测试首先测试其功能代码。功能代码不多也不少差好通过测试,这就是我们说的单元测试。
客户(类)测试:测试工程师编写测试代码从用户的观点来对功能代码进行验证。这也可以被称作接收测试。
在接下来的文字中我个人将为大家讲解程序员测试,这种测试是程序员的职责所在。
恰到好处
在定义程序员测试的时候我提到过程序员测试的目标就是功能代码不多也不少。其实说起来容易做起来难。这是一个自然趋势,特别是对程序员来说,尝试(在项目中)找出新功能。有多少次在你编写代码的时候不得不加入一些“特性”因为你知道实际上这些“特性”以后将会用到。可能你写了一个方法来得到用户订单概要,虽然它并不是当前需求的一部分,但很快就会需要的。
这是我们必须避免的陷坑。Albert Einstein以前说过:“愚蠢的人把事情搞大搞复杂搞乱,这只能由一堆天才加一堆勇气才能把他纠正过来。”换句话说,真正好的代码就是完成需求的代码,没有多的。这些功能代码将成功通过所有的测试,(它们之间)有清晰的通信,包含最少的可能的类和方法。当这些代码完成了它们应该完成的,请远离键盘,抵挡住想添加更多(代码)的冲动。
这样很不错,不过应该如何做呢?
作为一个热衷于使用TDD的程序员,你要做的第一件事就是用单元测试来文档化功能需求。在任何功能代码编写之前就应该做这个。从建立一个测试列表开始,描述在需求中定义的所有测试,确保此列表对于需求标准来说是最完整的。确保此列表尽可能的简单同时也能反映需求的文档化。
比如,我们来假设在程序中我们要创建一个对象来表示产品。这个测试列表看起来可能像这样:
<!--[if !supportLists]-->1, <!--[endif]-->创建一个Product,验证IsDirty是否为假;
<!--[if !supportLists]-->2, <!--[endif]-->创建一个Product,修改它的产品名,验证IsDirty是否为真;
<!--[if !supportLists]-->3, <!--[endif]-->创建一个Product,修改它的产品名,验证ProductName是否被修改;
<!--[if !supportLists]-->4, <!--[endif]-->创建一个Product,修改它的产品名,保存此产品,验证此产品是否被保存;
这是测试列表的一个简单简单示例,主要是因为没有太多的需求。
红/绿/重构
红/绿/重构是TDD指示,这描述了你使用TDD方式开发过程中经历的三个状态。
红:测试失败;
绿:编写的代码通过测试;
重构:在不改变功能的前提下优化代码;
你也可以这么说:“测试失败,测试成功,优化之”。让我们来看看实际中的情况。
编写程序员测试
让我们来完成测试列表中的第一个需求(测试)。在Visual Studio 2005 Team System中假设你有开发人员版或是测试人员版,你可以创建一种新的项目,测试项目。
在测试项目经过少量的手工流程后我开始关注UnitTest1.cs文件。你将会把你用C#写成的需求放置于此。删除 ManualTest1.mht 文件 — 它对此项目并无用处。
using System.Text;
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace DotNetJunkiesTDD {
[TestClass]
public class ProductTest {
public ProductTest() {
// TODO: Add constructor logic here //
}
[TestMethod]
public void TestMethod1() {
// TODO: Add test logic here
}
}
}
在代码中你可以看到测试代码的基础结构。这是一个指定了包含程序员测试(单元测试)的类。此文件引用了 Microsoft.VisualStudio.TestTools.UnitTesting 这个命名空间,这样就给此单元测试类带来了很多帮助。
类定义带有[ TestClass ] 属性,这就说明了此类是单元测试的一个子集(如果你用过NUNIT,那它和TestFixture就是一回事)。
方法定义带有[ TestMethod ]属性,这就说明了此方面是代表单一功能单元测试。单一特性(比如产品在代码中的表示)包含了多个程序员测试(在NUNIT中是Test)。
红
作为程序员测试的第一个需求,你在 TestMethod1 方法中编辑了如下代码:
/// Create a Product and verify IsDirty is false.
/// </summary>
[TestMethod]
public void CheckIsDirty() {
Product prod = new Product();
Assert.IsTrue(prod.IsDirty);
}
作为一个 TestMethod 你定义了一个 CheckIsDirty 的方法。在测试中你只能通过编写测试代码来达到你的测试标准。在这儿你为Product类创建了一个新实例,而且使用 Assert.IsFalse() 方法来进行一个 IsDirty 属性 是否为假的断言。当然如果你想编译这段代码是编不过的,因为Product类你还没有定义。在此阶段你是红,你没有通过测试(编译失败也是失败)。
绿
第二步是编写刚能满足需求的代码(用来通过测试)。要完成此事你可以新建一个用于放置Product类的类库项目,建立Product类。
using System.Collections.Generic;
using System.Text;
namespace MyLibrary {
public class Product {}
}
在添加完Product类(以及在测试项目中添加引用)后仍然没有完成编译因为我们没有通过测试。我们必须要在属性 IsDirty 中指明返回假。
public bool IsDirty = false ;
}
此时上述代码已经符合我们的需求定义了。我也能很容易的添加一些别的代码(比如从这个属性里返回真),但目前我还没有收到这方面的需求。
TDD开发的一个重要原则是小步增量开发软件。你在测试中文档化你的需求,只写够用的代码来满足测试,必要时清除(测试代码);然后是下一个需求。编写刚好满足需求的代码(比如使测试通过)是需要很多练习的。
既然我们有了这些代码,两个项目都可以被编译了。一旦项目被编译你就可以运行测试来看它们是否通过测试(满足需求)。在VSTS工具栏上点击绿色向右的箭头,如下图所示:
在运行测试的时间,测试结果窗口出现,在将要运行的测试旁边会有一个感叹号。当测试开始运行的时候你会看见它变成绿色带勾的形状,或是红色带X。绿色表示通过,红色表示失败(你不会现在才开始找“红/绿/重构”在哪提到过吧?)
只要我们的功能代码是刚好能通过我们需求测试的代码,我们就得到绿色标志,从而进入下一步。
重构
下一步是在可能的情况下重构代码。让我们回去看看Kent Beck的规则,这就是“消除重复”规则。重申,重构的定义是在不改变功能的前提下优化代码。通过对我们编写的代码进行代码复查发现没有可重构的地方。
如此往复
然后回到你的测试列表再编写下一个需求的程序员测试。
/// Create a Product, change the Product Name, and verify IsDirty is true.
/// </summary>
[TestMethod]
public void ChangeProductName() {
Product prod = new Product();
prod.Name = " New name " ;
Assert.IsTrue(prod.IsDirty, " After changing Product.Name, Product.IsDirty should be true " );
}
以上代码将会失败因为我们没有 Name 属性。下一步是编写足够的代码使其能顺利编译。
public bool IsDirty = false ;
public string Name;
}
在写完代码两个项目都编译完后。下一步你运行你的单元测试— CheckIsDirty 属性通过(没有任何改变)但 ChangeProductName (属性)失败了。
当此代码被编译后,我们仍然没有足够的代码来满足需求(仍然是红色阶段),那我们就加代码满足之:
public bool IsDirty = false ;
public string Name {
set { IsDirty = true ; }
}
}
编译两个项目后,两个测试都通过了,我们满足了两个需求。是重构的时候了。尽量通过查看代码使之更可能清晰,但在测试项目中有一些重复代码,我们可以按以下这样重构(是的,我们连测试代码也重构):
public class ProductTest {
public ProductTest() {
// TODO: Add constructor logic here
}
Product m_prod;
[TestInitialize]
public void Init() {
m_prod = new Product();
}
/// <summary>
/// Create a Product and verify IsDirty is false.
/// </summary>
[TestMethod]
public void CheckIsDirty() {
Assert.IsFalse(m_prod.IsDirty);
}
/// <summary>
/// Create a Product, change the Product Name, and verify IsDirty is true.
/// </summary>
[TestMethod]
public void ChangeProductName() {
m_prod.Name = " New name " ;
Assert.IsTrue(m_prod.IsDirty, " After changing Product.Name, Product.IsDirty should be true " );
}
}
你可以使用[ TestInitialize ]属性修改一个方法指明在所有测试方法之前优先运行此方法( ClassInitialize 属性将指明在第一次测试开始的时候此方法被优先执行)。
现在我们已经完成了“红/绿/重构“这一步了,可以再回到测试列表上进入下一个需求。
/// Create a Product, change the Product Name, verify the ProductName is changed.
/// </summary>
[TestMethod]
public void ChangeProductNameVerify() {
string prodName = " Seven's Death Ray " ;
m_prod.Name = prodName;
Assert.AreEqual(prodName, m_prod.Name,
" After changing Product.Name, the value should be the same as the value passed in. "
);
}
注意我们有一个新的断言, AreEqual 断言。记的检查文档来学习你可能会使用到的断言。
因为 Product.Name 属性没有get访问所以代码不能通过编译,所以我们要添加一个:
public bool IsDirty = false ;
public string Name {
get { return null ; }
set { IsDirty = true ; }
}
}
当代码能编译了,新的测试又失败了。 Name 属性期望返回类似“Seven's Death Ray”但它却返回空,那么我们更新代码:
public bool IsDirty = false ;
private string m_name;
public string Name {
get { return m_name; }
set {
IsDirty = true ;
m_name = value;
}
}
}
现在所有的测试都通过了,我们再次检查有无重构的可能,然后再进入下一个测试。
总结
在本文中你学习了测试驱动开发以及如何在Visual Studio 2005 Team System中使用TDD开发软件。希望你能做一些小的练习,做一些和你平时做的不一样的事,你将会知道开发软件只用编写需要的代码就行。另外,当你写代码时,你会特别有信心,你不会因为任何的代码的添加、修改和删除产生破坏性的改变。如果所有的测试都通过了,就说明满足了需求。
我真心希望你能试试TDD,我想你会上瘾的。它看起来很笨重,实际上却不是,而且一点点额外工作带来的回报是很值得的。在本文中似乎有很多的步骤,而且要花不少时间写测试驱动代码。而实际上每个测试和代码只用花很少的几秒钟就可以搞定—看上去更久些是因为我花了很多此过程中的每一步。