处理数据库
迄今为止,我们只支持销售一种价格为$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 In