很多年来,开发人员一直在享受测试驱动开发(TDD,Test-Driven Development)所带来的便利。无论使用什么语言,现在都能轻松找到合适的工具——NUnit、JUnit以及为Perl、Python、Ruby、Delphi或其他语言所设计的各种各样xUnits框架。但是到了实现数据库逻辑的时候,我们的选择余地就少了许多。于是当他们真的希望做单元测试的时候,许多人只好选择自行开发单元测试的解决方案。
不过随着Visual Studio 2005的发布,SQL Server开发人员在这个问题上的境遇已经改变了。作为Visual Studio Team System的一部分,其为数据库专家所设计的版本(官方命名为“Visual Studio 2005 Team Edition for Database Professionals”)已经发布了,它为以下几个问题提供了答案:
虽然前三点非常重要——它们已经为团队解决了许多问题——但是第四点经常被忽视。因为单元测试和数据生成已经成为DBPro的头等公民,开发人员能够将存储过程与他们的测试驱动开发周期集成在一起。这给团队提供了强大的能力和自信来确定自己的系统运行良好,同时也能更深入地了解系统在数据库架构改变之后所受到的影响。
在我们探究使用DBPro进行测试驱动开发的方法之前,让我们先了解一下开发人员是如何在单元测试框架的帮助下创建业务逻辑的。假设我们有个需求是计算一个订单的折扣,折扣方式如下所示:
使用Visual Studio.NET,我们可以先创建一个测试项目并且编写业务逻辑的测试代码。我们编写的第一个测试可能是这样的:
[TestMethod] public voidOrderOfZeroDollarsShouldHaveZeroDiscount()
{
double orderAmount = 0.00;
double discountExpected = 0.00;
double actualDiscount = OrderDiscount.CalculateDiscountFor(orderAmount);
Assert.AreEqual<double>(discountExpected, actualDiscount);
}
而我们可以这样实现CalculateDiscountFor方法:
public static double CalculateDiscountFor(double orderAmount)
{
return 0.00;
}
然后我们就可以修改测试代码和方法实现,直到完全满足以上的折扣策略。然而,这意味着如果我们要改变折扣策略,就需要重新编译代码,至少也需要修改配置文件。
如果这段逻辑存储在一个数据表里,那么我们可以将订单的价格传入一个存储过程,然后在表中查询折扣数量。不过,当我们着手创建表格和存储过程时,很快就会遇到一些问题。这个表格的结构是怎么样的?我们该如何表示一个范围的最低值和最高值?如何处理边界情况?
这正是DBPro的单元测试功能试图回答的问题。让我们来看一下如何使用测试驱动的方式,在SQL Server中创建表格和存储过程并实现这个功能。如果您想随着以下的步骤一起进行试验,那么您需要安装Visual Studio 2005/2008 with Team Edition for Database Professionals (DBPro),以及Team Edition for Testers/Developers。您能够在http://msdn2.microsoft.com/en-us/teamsystem/default.aspx下载到180天试用版本。
首先我们需要一个数据库。DBPro中的测试是面向一个真正的数据库的。一般来说,单元测试应该避免涉及到文件系统,数据库以及其他一些外部资源(Feathers, Michael, Working Effectively with Legacy Code, Prentice Hall PTR, 2004),因为这样会降低测试的速度。然而,您也许可以将这些测试视为集成测试,这样测试所带来的功效就弥补了速度方面的问题。为了提高测试速度,我们将在本地的SQL Express数据库中运行这些测试。
在Visual Studio的菜单中选择View -> Server Explorer。右键单击Data Connections并选择“Create New SQL Server Database”:
输入您的数据库服务器(在这个例子中,我们使用(local)\SQLEXPRESS)并输入数据库名称“OnlineStore”:
您现在应该可以看到Server Explorer中列出的数据库连接。下一步我们需要建立一个项目来编写我们的业务逻辑。在Visual Studio中,选择菜单中的File -> New -> Project。在Project Type栏目中,您应该能够看到一个“Database Project”条目。展开之后您会看到一个“Microsoft SQL Server”条目,点击它并选择SQL Server 2005 Wizard。然后将项目命名为OnlineStore:
当您点击OK之后,您可能会得到一个警告信息,表明您的SQL Server不支持全文索引。这是因为SQL Express并不支持这个功能。如果您是按照目前的步骤在执行,就可以放心地忽略这条信息。
我们现在进入了SQL Server 2005向导。您可以不断点击每个窗口中的Next按钮,直到出现Configure Build/Deploy界面。DBPro事实上是我们数据库的一个离线表现形式,我们可以使用与其它.NET项目非常相似的方式来部署这个数据库项目。点击Target Connection旁边的Edit按钮,并选择我们刚建立的数据库。
正确填写各种信息之后,点击OK按钮,然后点击向导中的Finish按钮。最后您将会看到一个摘要页面展示了向导中的设置。当项目创建完成后再点击Finish按钮。
到目前为止,我们有了一个建立在SQL Express上的测试数据库,以及一个数据库项目。这个项目既表现了数据库的离线状态,也是一个我们用于确定数据库真实架构的地方。现在我们已经做好编写业务逻辑的准备了。右键单击我们的解决方案并选择Add -> New Project。添加一个新的Test Project,将其命名为OnlineStoreTests。
这样就会在解决方案中添加一个测试项目。您可以关闭那些已经打开的文件,并删除AuthoringTests.txt、ManualTest1.mht、UnitTest1.cs等自动生成的文件。下一步,右键单击Test项目并且选择Add -> New Test。在Add New Test对话框中,选择Database Unit Test并将其命名为OrderDiscountTests.cs:
当您点击OK按钮之后就会弹出一个配置向导,让我们选择一个用于运行测试的数据库连接。它也会让我们选择第二个连接用于验证这些测试。这在某些情况下非常有用,例如一个测试应该作为一个普通用户来执行,但是存储过程可能修改了这个用户账号本不能访问的数据表。
现在,我们需要选择一个之前建立的数据库连接用于执行单元测试,因此我们从下拉框中选择OnlineStore连接。由于我们将会在编写测试数据库项目中开发我们的存储过程,所以我们也要在测试运行之前建立配置信息,用于自动部署我们对数据库项目的改动。这会导致测试在运行前有所延迟,但是这避免了因为没有重新部署数据库的改动而使测试莫名其妙的通过或失败。现在您的屏幕应该是这样的:
请注意,我们也可以在单元测试运行之前生成测试数据。这个功能之强大已经超出了文章所描述的范围,但是我建议您可以对其进行深入研究。配置完成之后请点击OK按钮。现在您会发现Visual Studio中出现了三个编写单元测试的重要窗口。第一个是当前测试区域:
这个区域中有几个下拉列表,用于显示我们当前正在关注的测试(可能我们正在检查这个测试,或测试前后所要执行的脚本),以及一些用于添加、删除和重命名测试的按钮。
第二个区域是我们编写测试的主要窗口。它显示了如下消息:
正如我们将会看到的那样,测试将会使用T-SQL来编写。下一个区域是Test Condition部分:
这是在测试脚本执行之后我们所指定的一些操作,就好像在xUnit测试框架中的Asserts语句一样。
我们在开始之前还需要做一步整理工作。当我们创建了OrderDiscountTests类之后,它将会为我们创建一个默认的测试。点击Rename按钮,并将其命名为ZeroDollarOrderShouldHaveZeroDiscount。现在,点击“Click here to create”按钮,删除注释,并输入以下代码:
exec sp_calculate_discount_for_order 0.00
我们希望测试调用我们的存储过程,并使用$0.00作为订单价格。基于我们在文章开始时所提到的对应表,返回的折扣数量应该是0.00。因此,我们需要添加一个Test Condition来比较我们的期望值和返回值。在我们的Test Condition部分中,删除Inconclusive Result(点击红X),然后在下拉列表中选择Scalar Value并点击添加按钮。
这允许我们把期望值和结果集中特定行和列的值进行比较。右键单击这一行并选择Properties,并在属性面板中进行修改,这里我们希望第1行第1列的值为0.00。
现在,选择Test菜单中的Run(在2005中选择run without the debugger)运行我们的单元测试。您会发现运行测试需要一段时间,这是因为它正在比较我们的数据库项目和目标数据库,来确定是否需要进行部署或者进行其他一些需要在第一次运行时处理的配置。您应该看到测试失败的提示,因为数据库中缺少sp_calculate_discount_for_order存储过程,那么让我们来解决这个问题。右键单击数据库项目并选择Add -> Stored Procedure:
将其命名为sp_calculate_discount_for_order并点击OK按钮。请注意这是个存储过程的SQL语句定义,将其修改为:
CREATE PROCEDURE [dbo].[sp_calculate_discount_for_order]
@orderAmount money
AS
SELECT 0.00
RETURN 0;
在窗口外我们能够得到分析ResultSet的支持。现在我们的存储过程及将会返回我们期望的折扣数值。每次您改变存储过程之后,都需要保存文件。这时候我们数据库项目中已有了存储过程定义,但是数据库中还没有。您打开目标数据库就会发现现在还没有任何存储过程。我们再回到测试中:
测试通过了!您可能运行测试前又出现了一次停顿,这是因为数据库项目中的改变正部署到测试用的目标数据库。
让我们添加另外一个测试来检验折扣为0的另一个边界条件。在您的测试屏幕中,点击绿色的加号并且将新测试命名为NinetyNineNinetyNineOrderShouldHaveZeroDiscount。修改脚本,使它调用我们的存储过程,并且修改Test Condition以验证存储过程返回的折扣数值为0:
设置完毕后重新运行测试,您应该会发现这次运行快了不少,而且两个测试都通过了:
现在我们来为下一个折扣级别编写测试。价值在$100.00和$299.99之间的订单会获得2%的折扣。添加一个名为OneHundredDollarOrderShouldHaveTwoPercentDiscount的新测试。现在,我们所期望的数值应该为0.02:
现在再重新运行测试:
前两个测试通过了,但是第三个失败了,因为它期望0.02却获得了0.00。我们可以改变存储过程的逻辑使测试通过,但是因为我们已经在数据库中了,那么就建立一个表格来存储这些值吧。右键单击数据库项目并选择Add -> Table,使用如下的定义创建一个OrderDiscounts表:
CREATE TABLE [dbo].[OrderDiscounts]
(
low_range float NOT NULL,
high_range float NOT NULL,
discount_amount float NOT NULL
);
因为我们希望控制测试业务逻辑时所使用的表中的数据,所以我们会创建一段脚本,在运行所有测试之前向数据表中插入合适的数据。在OrderDiscountTests.cs文件中,选择测试对应的下拉列表中的Common Scripts选项:
请注意下拉列表的旁边有两个选项——Test Initialize和Test Cleanup。我们目前只需要使用Test Initialize来建立数据库。请确认目前选择了Test Initialize并点击“Click here to create”链接,然后输入以下脚本:
INSERT INTO OrderDiscounts(low_range, high_range, discount_amount)
VALUES (0.00, 99.99, 0.00);
INSERT INTO OrderDiscounts(low_range, high_range, discount_amount)
VALUES (100.00, 299.99, 0.02);
INSERT INTO OrderDiscounts(low_range, high_range, discount_amount)
VALUES (300.00, 999.99, 0.04);
INSERT INTO OrderDiscounts(low_range, high_range, discount_amount)
VALUES (1000.00, 10000000.00, 0.07);
重新运行测试时,我们会发现一定的延迟,因为目标数据库中正在创建我们的数据表,不过最后一个测试依旧无法通过。现在我们就来把它变为绿灯。现在我们修改一下存储过程,让它从我们刚添加的数据表中获取折扣数量:
CREATE PROCEDURE [dbo].[sp_calculate_discount_for_order]
@orderAmount money
AS
SELECT discount_amount from OrderDiscounts
where @orderAmount between low_range and high_range
RETURN 0;
重新运行测试:
变绿了!然而,我们的测试还不可靠。回到我们的某个测试,并为它添加一个条件,确保我们的存储过程只会返回一条记录:
重新运行测试,它们都通过了吗?
失败了!不过更奇怪的是失败的原因:
9条记录?我们看看数据库中究竟发生了什么:
哇塞,好像我们的测试数据插入太多遍了。这是个非常重要的教训——当我们在操作数据库时,可能会没有意识到数据会始终存在并影响测试。让我们回头修改一下测试脚本来解决这个问题:
TRUNCATE TABLE OrderDiscounts;
INSERT INTO OrderDiscounts(low_range, high_range, discount_amount)
VALUES (0.00, 99.99, 0.00);
INSERT INTO OrderDiscounts(low_range, high_range, discount_amount)
VALUES (100.00, 299.99, 0.02);
INSERT INTO OrderDiscounts(low_range, high_range, discount_amount)
VALUES (300.00, 999.99, 0.04);
INSERT INTO OrderDiscounts(low_range, high_range, discount_amount)
VALUES (1000.00, 10000000.00, 0.07);
现在再重新运行以下测试,于是……
所有的测试都通过了!在我们完成剩余的测试用例之前,可能您还会希望了解一件事情。当订单的价值正好在上限或下限时工作完全正常,但是如果正处在某个级别的上限和下一级别的下限时又会怎么样呢?换句话说,如果某个订单的价值经计算为99.997会发生什么呢?在了解这个状况之前,我们先来设想一下如果这个情况真的出现时该怎么样。我们在OrderDiscoutTests文件里再添加一个名为NinetyNineNinetyNineNineShouldHaveZeroDiscount的测试。当然,您的业务可能会希望换种做法——超过$99.99的数值就被视作下一级别。执行我们的存储过程并添加一个新的Test Condition以确保返回0.00。
exec sp_calculate_discount_for_order 99.999
运行我们的测试,通过了吗?
没有。如果您查看错误信息,就会发现错误的原因是因为没有返回任何记录。我们可以改变插入至表格中的数据,但是如果其他某个人犯了同样的错误呢?根据我们的业务逻辑,我们似乎只需要保留2位小数就可以了,而money类型显得过于精确了一些。那么我们来修改一下存储过程:
CREATE PROCEDURE [dbo].[sp_calculate_discount_for_order]
@orderAmount float
AS
SELECT discount_amount from OrderDiscounts
where ROUND(@orderAmount, 2, 1) between low_range and high_range
RETURN 0;
重新运行测试:
绿的彻头彻尾!是时候实现其他的测试用例了,不过这就留给读者作为练习来做吧。
正如您所看到的,在Team Edition for Database Professionals中,熟悉驱动测试开发的开发人员能够继续使用“红灯——绿灯——重构”的开发方式来编写存储过程。对于那些不进行测试驱动开发的开发人员,也可以利用数据库的离线表现形式,以及单元测试功能来确保数据库内业务逻辑功能实现的正确性。
照顾两个女儿之余,Cory喜欢用测试先行的方式,使用Ruby,C#以及Java等各种语言进行开发。除此之外,他还在Code Camps和用户组中发表演讲。Cory目前在微软工作,担任Field Engineer,并且定期在他的blog(http://www.cornetdesign.com)中发表文章。
其他文章:
- 使用Team Edition for Database Professionals进行单元测试.
- Database Professional Team Center.
- VS2008下的测试驱动存储过程开发.
查看英文原文:Test Driven Development with Visual Studio for Database Professionals