单元测试-----------测试驱动的C/C++程序开发

我们后台开发中, 随着老员工渐渐离去, 新人必定要接替工作而接触新模块新代码, 新员工也必定会抱怨
单元测试太麻烦:
  一个单元测试, 其实就是和QA差不多的测试,必须搭起整个系统, 由于对系统不熟悉, 难免碰到很多问题, 于是埋怨四起.
  如果不这样测试, 自己又没能力找其它测试方法, 拿不出说服QA的测试案例来.
  我自己在前一年多的时间里也同样碰到这个烦人的问题, 我对此做了几件事:
  (1) 抱怨, 这是很多人共有的, 不过有些人说出来了, 有些人嘴里不说但心里骂而已.
  (2) 抱怨的同时, 提出自己对解决这一问题的方法:
      2.1 最早的时候我提出做一个类似测试组用的TestLink的保存功能点的系统, 那封邮件发给整个开发组了, 但没有任何人
  回应. 如果有这个功能点系统, 而且很好查, 那么今天你测试的时候, 发现一个已有的模块运行不了, 你可以很容易查到
  这个功能点的一切描述, 特别是配置问题.
      -----有些人说: 第一你想的这个不一定行, 但至少我碰到问题的时候我想了解决方法, 而不仅仅是抱怨! 第二你为什么
  不做一个出来, 原因是我对PHP不熟悉, 后来是XX手去做了, 至于现在还没出来和大家见面, 不得而知.
 
      2.2 我在对我们十年系统的代码难以阅读和难以调试的问题, 看了或正在看一些书和文章, 来寻求解决方法, 而不是每天
   做完了那几个项目经理规定的需求, 就回家睡大觉或者玩其它:
          如<<重构>>/<<Java与模式>>/<<Agile软件开发:  原则/模式和实践>>/<<修改代码的艺术>>/<<代码大全2>>
                而且在我所修改的bug中, 到处可见对代码的重构, 我尽力去做了, 至于效果让人去评说.
 
         2.3  根据<<修改代码的艺术>>里的提到方法,  我近期学用CppUnitLite:
                学会用这个工具不难, 知道怎么把你的代码写得能用这个工具测试就很难! 
                不知道抽象, 不知道解耦和隔离, 或者知道个名词, 就说自己会用JUnit和CppUnit/CppUnitLite来单元测试, 简直痴人说梦.  
 
    总之,  抱怨是正常的, 问题是你抱怨之后你想了什么, 做了什么.
===========================================================

Robert Koss, Jeff Langr

导言 

在过去两年里,我们公司发起、指导、培训极限编程(eXtreme Programming,XP)并提供咨询。我们相信XP是开发高质量软件的最佳技术,因为它强调沟通、简单、测试和快速反馈。通过许多业已被证实的实践,XP取得了应有的成就。在这篇文章里,我们只打算集中讨论这些实践之一 — 测试驱动的开发(Test Driven Development,TDD)。 

很多公司害怕因为这样或那样的原因无法做到“eXtreme”,但我们难以想象一个公司或过程(process)提倡不对其软件进行测试。TDD技术可以(也应该)应用于任何软件开发环境。实施TDD甚至不需要你的老板允许,因为它是作为编程活动的一部分而不是作为项目单独的一个阶段而完成的。尽管测试、设计和编程是三种截然不同的活动,但我们并不以“时间”来区分这些活动 — 它们是并行、持续地完成的。本文展示的例子示范了其运作方法。 

几乎所有关于XP或TDD的出版物都使用支持面向对象开发的编程语言。这是可以理解的,因为XP出身于Smalltalk社群并且许多(但不全是)公司事实上都在使用某种OO语言。 

我们最近的一个客户希望实施TDD。实际上,他们希望实施所有XP实践。问题在于他们公司使用的是C语言,并且在可以预见的将来将一直使用C语言,因此我们不得不卷起袖子来合计如何在C语言中受益于TDD。不幸的是,C语言不(直接地)支持多态,而后者正是TDD所使用的技术的一个基础性的概念。 

在TDD中,一次开发一个模块。为了正确地测试一个给定的模块,它必须与它所交互的其他大多数模块相隔离。在像C++和Java这样的OO语言中,我们可以使用多态接口来隔离测试模块,同时这些接口还为模块提供了一个测试环境。我们需要在C语言中具有同样的能力。 

我们利用在嵌入系统项目中的工作经验。其中,要么硬件还没有做好,要么硬件还没有到我们手上。在这些环境中,我们不得不“根除(stub out)”访问硬件的函数调用。为了能够像受益于支持多态的语言所提供的东西那样获益,这种“根除”技术正是我们需要的。实际上,“根除”就是多态,我们称之为连接期多态。 

TDD

TDD是这样的一种编程风格:先于被测试的代码编写测试。一开始测试甚至连编译也通不过,因为被测试代码都还没写呢。然后我们编写足够的代码让所有东西通过编译,但是故意编码致使测试失败。然后我们编写代码使这个测试通过。对于TDD过程来说,这每一步都非常关键。 

每当通过测试,我们就对代码进行重构,这样,代码将变成我们能够生产的质量最佳的代码。我们努力使代码对于迄今为止已经实现的功能而言具有最可能佳的设计 — 但不会超过该功能哪怕一点点。只有当完成朝着这些目标的重构,我们才继续前进去编写另外一个测试。在整个开发日里,我们快速持续地重复这个“测试—编码—重构”周期。 

你用一个小时编写代码,然后花费一整天使它通过编译和调试,想一想这样的情况发生有多么频繁?我们可不喜欢那么做,那样一天的大多数时间里都有一种失败感(因为没有在第一时间把代码写对)。不是编写一段(可能是很大的一段)功能代码,TDD要求我们工作于非常小的问题,解决之,然后继续前进到下一个非常小的问题。毕竟,每一个大而复杂的问题都是由一串小问题构成的。这么做,我们发现可以整天都保持成功。一个一个地解决小问题,让人可以忍受、感觉快乐并具有非常高的生产力。 

这一系列的成功的关键在于快速反馈。只要我们编写了足够的代码,有那么编译一次的希望,我们就运行编译器。每写一行代码就编译一次也许并不切合实际,但正如你可能想象的那样,这种做法离那也不远了。我们尽可能频繁地运行测试。我们的规则是不超过10分钟就运行所有测试一遍,5分钟更好,1分钟也不错。着迷于频繁地运行测试会强迫我们将工作分割为一系列非常小的问题。 

乍一接触你可能会感觉这种做法不自然,我们第一次学习TDD时当然也是如此。但是,在以这种风格编了两年程序以后,我们就决不会退回到老的编写代码的方式了,抑或以后再测试代码,抑或把代码交给质量保证部门进行测试。你尝试过对编写完的代码进行测试吗?这太难了,以至于常常根本不去测试。通过首先编写测试,我们保证代码经过测试并且系统被设计为可测试的。 

自从采用了TDD以后,我们几乎从来没有对我们编写的代码使用调试器。实际上,操弄调试器或者甚至添加一个printf语句都是一个失败的征兆:我们在硬啃一个超过我们咀嚼能力的更大的问题。对于可能需要的调试工作,我们的回应是,抛弃最近这几分钟的工作就是了,然后采用甚至更小的步幅继续尝试。 

销售点系统 

我们选择销售点系统作为一个例子(就像你可以在任何零售商店里看到的那种销售点系统)。售货员将商品放到扫描器下,读出条形码,而显示器则显示扫描的每一件商品的信息,并为每一位顾客跟踪每一笔销售(有一件或多件商品)的总额。 

我们第一个用户故事(User Story)[1] 

一笔销售中所有商品的总价是可用的。 

我们有一个Sale概念,并且知道它有一个总价(total)。让我们在一个测试中“捕获”这一点。我们本质上其实是对需求文档进行编码 — 它将作为一套单元测试而执行!在实践XP时,这是一个重要的概念。我们不生成看起来从不匹配工作代码的厚厚的纸质需求或设计文档。然而对于软件开发来说,将需求或设计纪录成文档的确又不可或缺,因此我们以编码的方式代替之。 

以代码的方式编写测试的不可或缺性还有第二个原因:手工执行测试极其耗时。最终将变得不可能保持甚至对一个尺寸不怎么样的代码体执行所有测试。我们必须使这个测试自动化,否则它们就难以运作。只要测试跑不起来,程序里就藏有缺陷。 

在TDD中,测试框架是一个不可或缺的工具。它不但为我们提供了自动执行测试的手段,同时还是每分钟开发过程的主要构成部分。这样的工具无需昂贵的投资。我们在这儿使用的工具 CppUnitLite是一个基于宏的工具,可以到我们的Web网站免费下载(www.objectmentor.com)。CppUnitLite是为C++设计的,但由于C++可以调用C,因此同样可以用它来开发C应用程序。而且,由于它只使用了最小限度的C++,因此,只有一丁点儿或者甚至没有任何C++知识的C开发者都可以迅速学会它的用法。 

对于我们的用户故事(user story)来说,我们首先创建SaleTest.cpp文件来容纳对Sale的测试。CppUnitLite需要一点点样板代码: 

#include "../TestHarness/TestHarness.h"

int main()
{
    TestResult tr;
    TestRegistry::runAllTests(tr);
    return 0;
}

函数main担当测试驱动器。基本思想在于runAllTests方法负责抽取和执行我们定义的所有测试。无需知道测试框架的工作机制就可以使用之。实际上,远在学员学习足够的C++知识来理解其工作机理之前 — 从我们C++培训班开课的第一天起就使用了这个框架。 

现在,样板代码已经就绪,可以编写第一个测试了。每一个测试都是由TEST宏构建而成的,它也是CppUnitLite的一个组成部分。使用这个宏定义的任何东西都将自动被runAllTests拾取。这个宏带有两个参数:一个用于对测试进行分组的任意名字,以及一个关于测试的描述性的名字。 

#include "../TestHarness/TestHarness.h"

int main()
{
    TestResult tr;
    TestRegistry::runAllTests(tr);
    return 0;
}

TEST(Sale, totalNewSale)
{
    CHECK(0 == GetTotal());
}

第一个测试的名字是totalNewSale。它表示在任何Item被添加进来之前的一个新的Sale的状态。在任何东西被购买之前Sale的总价为0看起来合情合理,因此我们在测试中将此思想表达出来。 

注意,所有测试都包含一个或多个CHECK宏。我们使用CHECK宏来确保一个布尔条件值为true。一个测试可能包含不止一个CHECK语句,每一个都将被执行,而不管先前的CHECK语句成功与否。 

SaleTest当然无法编译,因为函数GetTotal还没有写出来呢。无论如何,我们先编译一回,以获得第一次反馈。如果SaleTest编译通过了,我们便会知道碰到了严重的配置问题。 

g++ -c SaleTest.cpp
SaleTest.cpp:
    In method 'void totalNewSaleSaleTest::run(TestResult &)':
SaleTest.cpp:12:
    implicit declaration of function 'int GetTotal(...)'

一旦得到了编译错误反馈,我们就创建刚好够用的Sale.h: 

#ifdef __cplusplus
extern "C"
{
#endif

int GetTotal();

#ifdef __cplusplus
}
#endif

我们将Sale.h包含进SaleTest.cpp中,并且创建刚好够用的Sale.c: 

#include "Sale.h"

int GetTotal()
{
    return -1;
}

注意在Sale.h中对连接指示符extern "C"的使用。这是必需的,因为我们的测试框架使用C++编写而成并且#include了这个文件,而Sale.c使用C语言编写而成并且也包含了这个文件。对于C++代码调用C代码而言,需要这个机制。 

我们的目的是为了通过编译,以便能够连接和运行测试。为了运行测试,只要简单地执行SaleTest即可: 

Failure: "0 == GetTotal()" line 13 in SaleTest.cpp 

发生了一处失败。 

这在意料之中:我们编写了一个测试,但还没有写什么产品代码。不过,我们希望看到一个失败,以作为“反馈驱动、测试先行”的设计周期的一个组成部分。对此周期的补充提醒如下: 

  • 为尚未存在的代码编写一个测试。

  • 预期测试将会失败。

  • 编写刚好够用的代码使测试通过。

  • 重构。

每次一旦预期失败的测试通过了,对于我们而言都是一个震动! 

对于Sale应用来说,只要让GetTotal返回0就可以使得这个测试通过: 

int GetTotal()
{
    return 0;
}

尽管仅仅返回一个“硬编码”的值来让测试通过看上去荒谬可笑,但这正是TDD的一个组成部分。我们有一个失败的测试案例,我们希望尽可能快地通过测试。一旦测试通过,我们就会关心怎么来使它“正确”。返回0看起来是“错误”的,因为“硬编码”的0很快就会被取代掉。仅仅提供刚好够用的代码使得当前测试通过这一事实,意味着我们将必须编写更多的测试。这些测试将会失败,又证明需要更复杂的算法。我们以这种方式稳步增加测试范围。进行更多的测试是一件好事。 

现在不存在测试失败了。 

第一个测试完成了。我们知道了当对它什么也没有做时一个Sale的状态应该是什么样子。现在,我们希望具有销售至少一件商品的能力,因此编写测试sellOneItem。对于这个测试而言,我们虚构一件商品,其条形码为“a123” ,价格为199美分。(计算到分易于避免以后任何浮点数的四舍五入问题) 

TEST(Sale, sellOneItem)
{
    BuyItem("a123");
    CHECK(199 == GetTotal());
}

为了销售一件商品,需要将其条形码字符串传入Sale。我们设计一个函数BuyItem来完成这项任务。购买条形码为“a123”的商品应该会将Sale的总价增加至199。编译之并预期失败的发生: 

SaleTest.cpp:18:
    implicit declaration of function 'int BuyItem(...)'

为了通过编译,必须将头文件修改为: 

int GetTotal();
void BuyItem(char* barCode);

并将Sale.c修改为: 

#include "Sale.h"

int GetTotal()
{
    return 0;
}

void BuyItem(char* barCode)
{
}

现在可以运行所有测试并预期sellOneItem发生失败: 

Failure: "199 == GetTotal()" line 19 in SaleTest.cpp 

发生了一处失败。 

现在是添加代码使这个测试通过的时候了。每一位顾客的购买货款必须从总价(total)为0开始。从这一点来说,可以工作的最简单的东西就是引入一个变量来跟踪总价。我们在Sale.c中以static变量方式声明之,这使得变量只具有文件范围的可见性。 

在Sale.c中,我们从GetTotal中返回total,并在BuyItem中更新之: 

#include "Sale.h"

static int total = 0;

int GetTotal()
{
    return total;
}

void BuyItem(char* barCode)
{
    total = 199;
}

现在两个测试都通过了[2]。 

在每一个新的测试通过以后,我们总是暂停一下,检查代码以确保其清晰。我们看到在sellOneItem中存在字符串字面量“a123”以及魔术数字199,并且感觉代码并没有尽其可能地清晰。因为测试将会发展演化并将连同被测代码一起被维护,我们希望它们能够充当关于如何操纵Sale函数的优秀的文档。我们坚信可以良好沟通的代码远远胜过往代码里添加注释,因为注释很快就会过时。我们向测试代码里引入局部变量: 

TEST(Sale, sellOneItem)
{
    char* milkBarcode = "a123";
    int milkPrice = 199;

    BuyItem(milkBarcode);
    CHECK(milkPrice == GetTotal());
}

现在我们可以继续前进并编写第三个预期失败的测试。注意已经有复用milkBarcode 和milkPrice的常量声明的需求了,因此我们将它们移出sellOneItem,并将其声明为static。 

TEST(Sale, sellTwoItems)
{
    BuyItem(milkBarcode);
    BuyItem(milkBarcode);
    CHECK(2 * milkPrice == GetTotal());
}

正如意料,它失败了: 

Failure: "2 * milkPrice == GetTotal()" line 29 in SaleTest.cpp 

存在一处失败。 

在Sale.c中创建正确的产品代码看上去很简单。不是简单地将total设为milk的价格,我们将价格计入total: 

void BuyItem(char* barCode)
{
    total = total + 199;
}

我们再一次编译运行测试并预期成功。这一次有点让人惊讶,因为得到了同样的错误。 

挠了挠头之后,我们怀疑static total可能没有被初始化。我们经由测试(而非调试器)来探究答案: 

TEST(Sale, sellTwoItems)
{
    CHECK(0 == GetTotal());
    BuyItem(milkBarcode);
    BuyItem(milkBarcode);
    CHECK(2 * milkPrice == GetTotal());
}

哎呀,这一次我们收到了两处失败反馈。第一处失败确定了我们的怀疑:在sellTwoItems起始处,total没有被初始化: 

Failure: "0 == GetTotal()" line 27 in SaleTest.cpp
Failure: "2 * milkPrice == GetTotal()" line 30 in SaleTest.cpp

我们非常喜爱TDD给予的快速反馈! 

TestHarness的工作方式是每一个TEST都假定彼此独立。实际上,你无法保证测试将以任何特定的顺序进行。(这真的给我们引来了一些问题,参见[2])每一个测试都需要构建其自己的设置。 

我们憎恶一整天的调试劳动起因于一个测试在不知情的情况下处理某个先前的测试残留的问题。然而,如果你遵从“只编写刚好够用的代码使得测试通过”的原则的话,你将被强迫加入更多的测试。通常这些附加的测试会揭露诸如此类的隐秘问题。 

对于Sale例子来说,我们编写刚好够用的代码来处理一件且仅一件商品的销售问题,这将强迫我们为销售两件商品编写测试,而这暴露了我们的错误。为了修正这个问题,我们引入一个初始化函数,每一个测试 — 并且因此每一个将使用Sale的客户程序,都将必须调用它: 

static char* milkBarcode = "a123";
static int milkPrice = 199;

TEST(Sale, totalNewSale)
{
    Initialize();
    CHECK(0 == GetTotal());
}

TEST(Sale, sellOneItem)
{
    Initialize();
    BuyItem(milkBarcode);
    CHECK(milkPrice == GetTotal());
}

TEST(Sale, sellTwoItems)
{
    Initialize();
    CHECK(0 == GetTotal());
    BuyItem(milkBarcode);
    BuyItem(milkBarcode);
    CHECK(2 * milkPrice == GetTotal());
}

在Sale.h中声明好Initialize之后,我们将其定义添加进Sale.c中: 

void Initialize()
{
    total = 0;
}

所有测试都从这一点开始运行。 

处理数据库 

迄今为止,我们只支持销售一种价格为$1.99的商品。让我们加入第四个测试,确保可以处理销售两种商品: 

TEST(Sale, sellTwoProducts)
{
    Initialize();

    char* cookieBarcode = "b234";
    int cookiePrice = 50;

    BuyItem(milkBarcode);
    BuyItem(cookieBarcode);

    CHECK(milkPrice + cookiePrice == GetTotal());
}

编译并运行测试,失败了: 

Failure: "milkPrice + cookiePrice == GetTotal()" line 46 in SaleTest.cpp 

存在一处失败。 

让这个测试通过是很简单的: 

void BuyItem(char* barCode)
{
    if (0 == strcmp(barCode, "a123")) total += 199;
    else if (0 == strcmp(barCode, "b234")) total += 50;
}

我们知道必须使用条形码在某个地方进行查找。由于已经通过了测试,我们现在可以重构查找操作,其间需注意始终确保所有测试仍然可以通过。重构是一点一点递增进行的,我们只修改极少量的代码,然后就运行测试以获得反馈。对代码进行剧烈的修改很容易引入缺陷。 

现在,我们前进的一小步是以“调用一个GetPrice函数”的形式为查找引入一个占位符。BuyItem中的代码展示了我们希望做的事情。真正的运作细节将于稍晚一会儿给出。 

void BuyItem(char* barCode)
{
    int price = GetPrice(barCode);
    total += price;
}

换句话说,我们的目的在于某个别的函数将会完成根据条形码查找价格的任务。这就是广为人知的目的编程(programming by intention)。这是一个极具威力的技术,它强迫我们工作于愈来愈小的问题,直到“该如何做”显而易见为止。眼下,让我们将条件逻辑从BuyItem移入GetPrice函数: 

int GetPrice(char* barCode)
{
    if (0 == strcmp(barCode, "a123")) return 199;
    else if (0 == strcmp(barCode, "b234")) return 50;
}

我们将这个函数的声明添加进Sale.h并且运行测试以确保它们仍然可以通过。 

现在,GetPrice实际上将会到哪里去查找商品的价格呢?当然是到数据库里。 

但是,使用一个真正的数据库来进行开发是一件很大的事儿。我们不想“上升”到一个活生生的数据库,因此必须获得一个数据库工作快照(working snapshot)。我们还必须跟踪数据库管理员(DBA)对真正的数据库做出的任何修改,以保持这个快照时新。 

关于数据库问题还有一个性能方面的考虑。每一次测试我们都不得不去连接它,而这要占用大量的处理时间,然而我们又需要每隔几分钟就运行测试一次。建立数据库连接的缓慢性将会显著减少我们每天所能完成的工作量。 

我们希望“根除”到数据库的调用。BuyItem关心的全部事情只是GetPrice返回商品的价格。它并不关心价格是来自对数据库的查询还是GetPrice当场虚构的一个随机价格。当然了,当使用TDD来构建真正的数据库代码时,我们应该测试真正的GetPrice。 

为了使得对GetPrice的调用既能对产品代码的数据库查询有效,又能为我们的测试提供需要的数据,我们必须间接访问它。从一个函数获得不同行为的常见方式是使用函数指针。我们的测试将把指针设置为指向其自己的存根查找函数,而产品代码则将指针指向一个进行实际数据库查询的函数。 

在SaleTest中,我们先提供一个存根函数(stub function)GetPriceStub用于查找价格: 

int GetPriceStub(char* barCode)
{
    if (0 == strcmp(barCode, "a123")) return 199;
    else if (0 == strcmp(barCode, "b234")) return 50;
}

我们的测试需要将指向这个存根函数的指针作为参数传递给Initialize。我们将每一个测试中的Initialize修改如下: 

Initialize(&GetPriceStub); 

这将通不过编译。我们修正Sale.h和Sale.c里的Initialize的原型和定义。它的新签名如下: 

void Initialize(int (*LookUpPrice)(char* barCode)) 

这样一来,Initialize需要存储函数的指针。再一次,我们使用声明于Sale.c中的static变量: 

static int (*LookUpPrice)(char* barCode);

void Initialize(int (*db)(char*))
{
    total = 0;
    LookUpPrice = db;
}

最后,对GetPrice进行修改,这样它就可以提领(dereferences)数据库函数指针,以便调用数据库查询函数: 

int GetPrice(char* barCode)
{
    return LookUpPrice(barCode);
}

编译连接并运行测试。成功。 

现在我们可以选定的任何方式获得商品的价格。对于测试而言,我们使用一个简单的if/else结构基于已知的条形码来返回“硬编码”的价格。而在产品代码里,无论谁来构建销售点系统都必须提供一个指向Sale的函数指针,以便存取真正的数据库。(这个函数当然应该拥有它自己的一套测试!) 

连接期多态 

指针威力巨大但也危险无比,近十年来引入的主要语言倾向于将它排除在外就是一个证据。指针的问题在于我们必须确保它指向我们要它指向的地方。一个迷途的指针很容易导致程序崩溃并致使整个开发小组调试到半夜。因为提领(dereferencing)一个非法的指针确定无疑是一个bug并且很可能造成程序崩溃,所以很多程序员在对每一个指针进行提领操作之前都要测试其合法性,这样的检查使得代码难于阅读。 

对于使用指针指向不同的函数而言存在一个替代方案:连接期多态。我们改用连接器来连接不同的函数。函数BuyItem(char*)将继续调用GetPrice(char*),不过将存在两个GetPrice(char*)函数定义:一个用于测试,我们将其放在测试文件SaleTest.cpp中,而另一个则被放于GetPrice.c文件中。 

当在整个开发日每几分钟就编译和连接测试时,我们连接SaleTest.o和Sale.o;当希望构建真实的系统时,我们连接真实的应用程序和GetPrice.o而非SaleTest.o。以下是一个非常简单的Makefile,描述了这两个构建操作: 

POSApp: Sale.o GetPrice.o main.o
    g++ -o POSApp Sale.o GetPrice.o main.o 

SaleTest: SaleTest.o Sale.o
    g++ -o SaleTest SaleTest.o Sale.o /
        ../TestHarness/TestHarness.a

SaleTest.o: SaleTest.cpp Sale.h
    g++ -c SaleTest.cpp

Sale.o: Sale.c Sale.h GetPrice.h
    gcc -c Sale.c

GetPrice.o : GetPrice.c GetPrice.h
    gcc -c GetPrice.c

结语 

在这个练习中,要注意的一个极其重要事情是我们在整个开发中都采用非常小的步幅。我们的目标是确保能够以一个稳步的速率递增地改善系统。我们希望看到测试每隔5到10分钟(甚至更短一些)就通过一次。这使我们免于沿着错误的方向花费太多的时间。在一天的结束,产品代码比一天的开始可以做的更多并且做的正确。 

适度应用TDD将会带来惊人的效果。我们认为它是当今软件开发的一个非常重要的“潮流”。我们不断遇到开发人员,他们告诉我们他们已经尝试TDD并且再也不会放弃。正如我们在这儿示范的,TDD并不仅仅适用于“对象”,你也可以在C语言中使用它。实际上,我们的观点是,由于C是如此具有威力但充满危险的语言(这要归因于它的“低级”天性),你真的必须使用TDD保护你自己。 

无可否认,我们选择的例子规模被缩小了以便描述TDD技术。然而,TDD已经被证实为可以处理巨大而复杂的系统。实际上,TDD的一个主要目标就是确保我们采用简化了的方式来处理复杂问题。利用TDD正确地管理复杂性,意味着我们可以构建“可以长期以合乎情理的代价而维护”的系统。 

Saletest.cpp(可以到www.cuj.com下载)展示的最终代码示范了连接期多态技术。 

附注 

[1] 一个用户故事(user story)是这样的一个需求:1.具有显而易见的业务价值;2.可被测试;3.可以在一次迭代中完成(通常是两周)。 

[2] 当在另外一台机器上编写这个Sale例子时,我们碰到了一些有意思的结果。测试以不同的顺序运行 — 并非从上到下。因为测试框架生成一个静态对象链接表,而在跨编译器和操作系统时,静态初始化并没有什么担保。在这种情况下,sellOneItem先于totalNewSale而执行,当totalNewSale执行时,变量total已经持有一个非零值,从而导致它失败。这可以稍微早一点给我们以反馈去正确地初始化变量。 

Robert S. Koss博士是Object Mentor公司资深顾问。他已经编程29年,最近15年使用C和C++。Koss博士已经指导了遍及世界数百个C、C++和面向对象设计课程,培训了数千名学员。 

Jeff Langr是Object Mentor公司资深顾问。他已经撰写了数篇文章连同《Essential Java Style》(Prentice Hall, 1999)一书。Langr具有20多年的软件开发经验,包括10年C++、Smalltalk和Java面向对象开发经验。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值