很多年来,开发人员一直在享受测试驱动开发(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”)已经发布了,它为以下几个问题提供了答案:
- 真正的数据库表现形式储存在什么地方。
- 如何让开发人员修改数据库架构,并且将这些更新以标准化的形式发送给 DBA 以供审批。
- 如何修改数据库架构(例如重命名一个数据列),并且让整个数据库都能了解这次改变所带来的影响。
- 如何对存储过程进行单元测试,包括如何为这些测试生成数据。
虽然前三点非常重要——它们已经为团队解决了许多问题——但是第四点经常被忽视。因为单元测试和数据生成已经成为 DBPro 的头等公民,开发人员能够将存储过程与他们的测试驱动开发周期集成在一起。这给团队提供了强大的能力和自信来确定自己的系统运行良好,同时也能更深入地了解系统在数据库架构改变之后所受到的影响。
在我们探究使用 DBPro 进行测试驱动开发的方法之前,让我们先了解一下开发人员是如何在单元测试框架的帮助下创建业务逻辑的。假设我们有个需求是计算一个订单的折扣,折扣方式如下所示:
- 0 到 99.99 美元 => 没有折扣
- 100 到 299.99 美元 => 2% 的折扣
- 300 到 999.99 美元 => 4% 的折扣
- 1000 美元以上 => 7% 的折扣
使用 Visual Studio.NET,我们可以先创建一个测试项目并且编写业务逻辑的测试代码。我们编写的第一个测试可能是这样的:
<span color="#006699">[TestMethod]</span> <span color="#0000ff">public void</span>OrderOfZeroDollarsShouldHaveZeroDiscount()<br></br>{<br></br><span color="#0000ff">double</span> orderAmount = 0.00;<br></br><span color="#0000ff">double</span> discountExpected = 0.00;<br></br><span color="#0000ff">double</span> actualDiscount = <span color="#006699">OrderDiscount</span>.CalculateDiscountFor(orderAmount);<br></br><span color="#006699">Assert</span>.AreEqual<<span color="#0000ff">double</span>>(discountExpected, actualDiscount);<br></br>}
而我们可以这样实现 CalculateDiscountFor 方法:
<span color="#0000ff">public static double</span> CalculateDiscountFor(<span color="#0000ff">double</span> orderAmount)<br></br>{<br></br><span color="#0000ff">return</span> 0.00;<br></br>}
然后我们就可以修改测试代码和方法实现,直到完全满足以上的折扣策略。然而,这意味着如果我们要改变折扣策略,就需要重新编译代码,至少也需要修改配置文件。
如果这段逻辑存储在一个数据表里,那么我们可以将订单的价格传入一个存储过程,然后在表中查询折扣数量。不过,当我们着手创建表格和存储过程时,很快就会遇到一些问题。这个表格的结构是怎么样的?我们该如何表示一个范围的最低值和最高值?如何处理边界情况?
这正是 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”按钮,删除注释,并输入以下代码:
<span color="#0000ff">exec</span> 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 语句定义,将其修改为:
<span color="#0000ff">CREATE PROCEDURE</span> [dbo].[sp_calculate_discount_for_order]<br></br> @orderAmount <span color="#0000ff">money</span><p><span color="#0000ff">AS</span><span color="#0000ff">SELECT</span> 0.00</p><br></br><span color="#0000ff">RETURN</span> 0;
在窗口外我们能够得到分析 ResultSet 的支持。现在我们的存储过程及将会返回我们期望的折扣数值。每次您改变存储过程之后,都需要保存文件。这时候我们数据库项目中已有了存储过程定义,但是数据库中还没有。您打开目标数据库就会发现现在还没有任何存储过程。我们再回到测试中:
测试通过了!您可能运行测试前又出现了一次停顿,这是因为数据库项目中的改变正部署到测试用的目标数据库。
让我们添加另外一个测试来检验折扣为 0 的另一个边界条件。在您的测试屏幕中,点击绿色的加号并且将新测试命名为 NinetyNineNinetyNineOrderShouldHaveZeroDiscount。修改脚本,使它调用我们的存储过程,并且修改 Test Condition 以验证存储过程返回的折扣数值为 0:
设置完毕后重新运行测试,您应该会发现这次运行快了不少,而且两个测试都通过了:
现在我们来为下一个折扣级别编写测试。价值在 $100.00 和 $299.99 之间的订单会获得 2% 的折扣。添加一个名为 OneHundredDollarOrderShouldHaveTwoPercentDiscount 的新测试。现在,我们所期望的数值应该为 0.02:
现在再重新运行测试:
前两个测试通过了,但是第三个失败了,因为它期望 0.02 却获得了 0.00。我们可以改变存储过程的逻辑使测试通过,但是因为我们已经在数据库中了,那么就建立一个表格来存储这些值吧。右键单击数据库项目并选择 Add -> Table,使用如下的定义创建一个 OrderDiscounts 表:
<span color="#0000ff">CREATE TABLE</span> [dbo].[OrderDiscounts]<br></br>(<br></br> low_range <span color="#0000ff">float NOT NULL,</span><br></br> high_range <span color="#0000ff">float NOT NULL,</span><br></br> discount_amount <span color="#0000ff">float NOT NULL</span><br></br>);
因为我们希望控制测试业务逻辑时所使用的表中的数据,所以我们会创建一段脚本,在运行所有测试之前向数据表中插入合适的数据。在 OrderDiscountTests.cs 文件中,选择测试对应的下拉列表中的 Common Scripts 选项:
请注意下拉列表的旁边有两个选项——Test Initialize 和 Test Cleanup。我们目前只需要使用 Test Initialize 来建立数据库。请确认目前选择了 Test Initialize 并点击“Click here to create”链接,然后输入以下脚本:
<span color="#0000ff">INSERT INTO</span> OrderDiscounts(low_range, high_range, discount_amount)<br></br><span color="#0000ff">VALUES</span> (0.00, 99.99, 0.00);<p><span color="#0000ff">INSERT INTO</span> OrderDiscounts(low_range, high_range, discount_amount)</p><br></br><span color="#0000ff">VALUES</span> (100.00, 299.99, 0.02);<p><span color="#0000ff">INSERT INTO</span> OrderDiscounts(low_range, high_range, discount_amount)</p><br></br><span color="#0000ff">VALUES</span> (300.00, 999.99, 0.04);<p><span color="#0000ff">INSERT INTO</span> OrderDiscounts(low_range, high_range, discount_amount)</p><br></br><span color="#0000ff">VALUES</span> (1000.00, 10000000.00, 0.07);
重新运行测试时,我们会发现一定的延迟,因为目标数据库中正在创建我们的数据表,不过最后一个测试依旧无法通过。现在我们就来把它变为绿灯。现在我们修改一下存储过程,让它从我们刚添加的数据表中获取折扣数量:
<span color="#0000ff">CREATE PROCEDURE</span> [dbo].[sp_calculate_discount_for_order]<br></br> @orderAmount <span color="#0000ff">money</span><p><span color="#0000ff">AS</span><span color="#0000ff">SELECT</span> discount_amount <span color="#0000ff">from</span> OrderDiscounts</p><br></br><span color="#0000ff">where</span> @orderAmount <span color="#0000ff">between</span> low_range <span color="#0000ff">and</span> high_range<br></br><span color="#0000ff">RETURN</span> 0;
重新运行测试:
变绿了!然而,我们的测试还不可靠。回到我们的某个测试,并为它添加一个条件,确保我们的存储过程只会返回一条记录:
重新运行测试,它们都通过了吗?
失败了!不过更奇怪的是失败的原因:
9 条记录?我们看看数据库中究竟发生了什么:
哇塞,好像我们的测试数据插入太多遍了。这是个非常重要的教训——当我们在操作数据库时,可能会没有意识到数据会始终存在并影响测试。让我们回头修改一下测试脚本来解决这个问题:
<span color="#0000ff">TRUNCATE TABLE</span> OrderDiscounts;<br></br><span color="#0000ff">INSERT INTO</span> OrderDiscounts(low_range, high_range, discount_amount)<br></br><span color="#0000ff">VALUES</span> (0.00, 99.99, 0.00);<p><span color="#0000ff">INSERT INTO</span> OrderDiscounts(low_range, high_range, discount_amount)</p><br></br><span color="#0000ff">VALUES</span> (100.00, 299.99, 0.02);<p><span color="#0000ff">INSERT INTO</span> OrderDiscounts(low_range, high_range, discount_amount)</p><br></br><span color="#0000ff">VALUES</span> (300.00, 999.99, 0.04);<p><span color="#0000ff">INSERT INTO</span> OrderDiscounts(low_range, high_range, discount_amount)</p><br></br><span color="#0000ff">VALUES</span> (1000.00, 10000000.00, 0.07);
现在再重新运行以下测试,于是……
所有的测试都通过了!在我们完成剩余的测试用例之前,可能您还会希望了解一件事情。当订单的价值正好在上限或下限时工作完全正常,但是如果正处在某个级别的上限和下一级别的下限时又会怎么样呢?换句话说,如果某个订单的价值经计算为 99.997 会发生什么呢?在了解这个状况之前,我们先来设想一下如果这个情况真的出现时该怎么样。我们在 OrderDiscoutTests 文件里再添加一个名为 NinetyNineNinetyNineNineShouldHaveZeroDiscount 的测试。当然,您的业务可能会希望换种做法——超过 $99.99 的数值就被视作下一级别。执行我们的存储过程并添加一个新的 Test Condition 以确保返回 0.00。
<span color="#0000ff">exec</span> sp_calculate_discount_for_order 99.999
运行我们的测试,通过了吗?
没有。如果您查看错误信息,就会发现错误的原因是因为没有返回任何记录。我们可以改变插入至表格中的数据,但是如果其他某个人犯了同样的错误呢?根据我们的业务逻辑,我们似乎只需要保留 2 位小数就可以了,而 money 类型显得过于精确了一些。那么我们来修改一下存储过程:
<span color="#0000ff">CREATE PROCEDURE</span> [dbo].[sp_calculate_discount_for_order]<br></br> @orderAmount <span color="#0000ff">float</span><p><span color="#0000ff">AS</span><span color="#0000ff">SELECT</span> discount_amount <span color="#0000ff">from</span> OrderDiscounts</p><br></br><span color="#0000ff">where ROUND</span>(@orderAmount, 2, 1) <span color="#0000ff">between</span> low_range <span color="#0000ff">and</span> high_range<br></br><span color="#0000ff">RETURN</span> 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
评论