DotNetNuke 私有程序集模块的数据驱动单元测试






4.50/5 (2投票s)
将数据驱动单元测试与 SQL Server CE 结合用于 DotNetNuke 私有模块开发。
引言
本文介绍如何使用 Visual Studio 2005 和 SQL Server CE 进行 DotNetNuke 私有程序集 (PA) 模块的数据驱动单元测试。
我 上一篇文章 介绍了如何为 DotNetNuke 私有程序集模块创建单元测试。本文是对该文章的扩展,涵盖了数据驱动单元测试的集成。数据驱动单元测试涉及使用外部数据源将测试数据馈送给您的单元测试,以便可以通过一段单元测试代码运行许多不同的数据值和组合。
数据驱动单元测试在提供比“硬编码”的单个单元测试更好的测试覆盖率方面要优越得多。这对于检查存储类型的边界数据(例如字符串长度、数字类型和其他重要的系统参数)特别有用。
本文专门介绍了如何使用我为 DotNetNuke 模块测试设置的单元测试类,但大部分信息将适用于任何类型的 Visual Studio 数据驱动单元测试。因此,即使您不在 DotNetNuke 中进行开发,这里可能也有一些对您有用的内容 - 毕竟,DotNetNuke 模块只是一个包含类型和数据访问方法的程序集。
指定外部数据源
要回答的第一个问题是:如何存储您的测试数据?显而易见的答案是 SQL Server - 毕竟数据库就在那里。它以正确的格式存储数据,其访问方法是众所周知的,并且如果您拥有 Management Studio,它还附带了一个不错的用户界面。
但是 SQL Server 有一个明显的缺点 - 它不易于移植。即使是 SQL Server Express 版本,您也可以移动一个 .mdf 文件,但您仍然需要在将要使用它的每台计算机上安装完整的应用程序。开发人员机器并不总是安装 SQL Server,即使安装了,也可能是错误的版本,权限也可能配置不同。然后,如果您想为您的开发环境引入一些质量控制(这正是您首先要实现单元测试的原因,对吧?),您可能希望有一个单独的、干净的构建机器来构建和部署您的成品代码。
所有这些都使 SQL Server 解决方案有点力不从心,因为它意味着需要在多台机器上维护多个 SQL Server 安装,或者确保所有机器都能访问单个服务器。最致命的是,很难将 SQL Server 数据库的更改与您的源代码控制系统保持同步。
另一种选择是将您的数据放入 Excel 电子表格中,并将其引用为您的测试数据。虽然我认为这很棒,因为它是一种轻量级、可移植的存储测试数据的方式,可以包含在源代码控制中,但在数据格式方面可能会有一些问题,并且要确保您的测试数据以正确的方式传输。如果您只是处理一些文本字符串和几个数字,这可能不会影响您。但是,如果您开始处理需要精确控制的财务/科学数据,您可能会遇到一些麻烦。
Microsoft Access 是一个不错的解决方案,我个人唯一的抱怨是:臃肿。即使是最简单的 Access 数据库,在几次打开/关闭后也可能会变得很糟糕。但是,如果您想存储和访问测试数据,将其集成到源代码控制中,甚至创建一个简单的界面,让非技术用户可以输入测试数据(例如,质量保证团队 - 有那样的团队不是很棒吗?),这是一个不错的解决方案。
不过,我个人最喜欢的还是 SQL Server CE。这项技术有多个名称,曾被称为 SQL Server Mobile、SQL Server Everywhere Edition,最后我认为微软已确定为 SQL Server Compact Edition (CE)。它基本上一直是同一个产品,但现在他们增加了桌面机支持,所以它不再仅限于 PDA。它非常轻量级,不需要在任何机器上安装,并且数据库通常保持很小的体积。您可以直接在 Visual Studio 中修改表,并且数据以与 SQL Server 中相同的类型存储。
本文的其余部分将重点介绍使用 SQL Server CE 作为测试数据源进行单元测试。
像往常一样,当我第一次踏上一条新路时,我总是会寻找那些先行者:快速的网络搜索会找到 使用 SQL Server 文件进行 VS2005 TS 单元测试,这是一个关于如何将 SQL Server CE 连接到您的单元测试的简单指南。
如何创建数据驱动的 DotNetNuke 模块单元测试
步骤 0:阅读第一篇文章并下载代码
上一篇文章 详细介绍了如何设置 DotNetNuke 模块的单元测试。在尝试将其设置为数据驱动之前,不要急于求成,直到这部分能够正常工作,并且您可以从 DotNetNuke 数据库中插入/删除/更新/选择数据。
本文的下载内容包含了第一篇文章中的所有库,以及一些新增的库。
步骤 1:下载 SQL Server CE 库
SQL Server CE 是微软提供的免费下载。您可以从微软获取所有详细信息。
- SQL Server 2005 CE 运行时:http://www.microsoft.com/downloads/details.aspx?FamilyID=85e0c3ce-3fa1-453a-8ce9-af6ca20946c3&DisplayLang=en
- Visual Studio 2005 SQL Server CE 工具(需要 VS 2005 SP1):http://www.microsoft.com/downloads/details.aspx?familyid=877C0ADC-0347-4A47-B842-58FB71D159AC&displaylang=en
- SQL Server 2005 CE SDK:http://www.microsoft.com/downloads/details.aspx?familyid=E9AA3F8D-363D-49F3-AE89-64E1D149E09B&displaylang=en
它们的安装过程并不算最友好,但应该会顺利进行。您只是在系统上安装了一些简单的文件 - 这不像 SQL Server Enterprise Edition 那样是大规模安装。此外,一旦您使用过一次,您就会发现 SQL Server CE 有各种各样的用途 - 它是一个方便的小型轻量级数据库。
步骤 2:创建单元测试项目
按照第一篇文章中的说明创建您的单元测试项目。简而言之:创建一个单元测试项目,引用 iFinity.Dnn.Utilities DLL,然后让您的 Unit Test 类继承自 DnnUnitTest
。您始终可以阅读 上一篇文章!
步骤 3:创建新的 SQL Server CE 数据库
您可以从 Visual Studio 菜单创建 SQL Server CE 数据库。打开“服务器资源管理器”,然后单击“连接到数据库”按钮。这是带有黄色圆柱体上方小绿色“+”号的那个。
此时应该会出现“添加连接”对话框。您需要更改数据源,因此请单击“更改...”这会弹出“更改数据源”对话框。您应该会看到“Microsoft SQL Server 2005 Compact Edition”的列表。如果您没有这个选项,您就卡住了。回到步骤 1 并重复,直到您在列表中看到它为止。(注意:如果您要在不同的技术中进行测试数据处理,您可以在此处选择它。)
将 SQL Server CE 选择为数据源
当您选择“Microsoft SQL Server 2005 Compact Edition”时,“数据提供程序”下拉列表应该会更改为“SQL Server Compact Edition 的 .NET Framework 数据提供程序”。同样,如果您没有这个选项,请回到步骤 1 并重复,直到您有为止。单击“确定”,您将返回到“添加连接”屏幕,只是这次它已经改变了。单击“创建”按钮来创建新的 SQL Server CE 数据库,或者如果您已经有一个可以使用的数据库,则单击“浏览”按钮。
创建新的 SQL Server CE 数据库
我通常会在测试项目旁边的相对位置(通常在一个名为 /db 的子文件夹中)创建我的测试数据库。这意味着您可以轻松地将数据库包含在源代码控制中,并使数据库的版本与使用它的单元测试代码版本保持同步。这也意味着在从源代码控制中提取代码到另一台机器时,您无需修改 app.config 文件来指定新的路径。如果您正在使用构建服务器,这一点尤其有用。
我从不为测试数据设置密码,因为这只会使事情变得更复杂,而且它只是测试数据。但如果您确实想要,可以创建一个密码。只需记住写下来以备后用。单击“确定”并创建您的数据库。
在 Visual Studio 的“服务器资源管理器”窗口中,“数据连接”部分现在应该有一个新数据库列表。
步骤 4:创建测试数据表
现在您已经有了数据库,您可以开始创建测试数据了。我只是使用服务器资源管理器创建表(请注意,如果您有 SWL Server Management Studio,也可以在其中打开 SWL Server CE 数据库)。我的表通常是我想要使用的测试数据的反规范化版本。例如,如果我的 DotNetNuke 数据库中有一个表结构是一对多的关系,我将只创建一个测试数据表来测试所有这些数据,并且我将以反规范化的形式输入测试数据。如果您不理解上一句话中的任何内容,请不要担心 - 您可能已经在生成反规范化数据了。
这段话的重点是为每个单元测试创建一个表。所以,如果您有一个单元测试类来测试 Widgets 的插入/更新/删除,那么您需要在您的测试数据数据库中创建一个 Widget 表。
在创建测试数据表时,我喜欢添加一些额外的列来描述正在进行的测试。一个重要的列是“TestOutcome”。您需要能够输入会失败的测试数据以及会通过的测试数据。根据您正在测试的内容,这可以是任何内容,从布尔值到捕获异常。我通常只将其创建为 varchar
字段,这样我就可以在其中放入任何我想要的内容并根据它进行编码。
为表创建列
我喜欢的另一个字段是“DeleteOnComplete” - 它只是一个布尔指示符,表示完成测试后是否删除测试数据。有时您想在完成后从数据库中删除测试数据,有时不想。通过在表中添加一列,您可以控制测试完成后是否删除该数据。它也可以是一个 varchar
字段,您可以在其中定义一个范围 - 例如“OnSuccess”、“OnFail” - 来确定是否删除测试数据。
对于其余的列,它们应该与您的目标 DotNetNuke 模块数据库表中的内容基本匹配。匹配名称和类型是最简单的,除非您要创建特定列来测试某个特定功能。否则,您基本上应该复制您的目标表。
一个不错的技巧是转到您的目标表并生成它的创建脚本(我使用 SQL Server Management Studio 来完成此操作)。然后,您可以将“Create table dnn_Widget (WidgetId int...
”复制粘贴到 SQL Server CE 查询窗口中,更改表名,删除“常规”SQL Server 语法(例如排序规则类型、索引等),然后运行它 - 这将在您的测试数据库中创建一个表的副本。只需将您的控制列添加到 create table 定义中,即可完成。您甚至可以将脚本保存在文本文件中,与数据库一起保存,以便将来轻松删除/重新创建。
现在您的测试表已创建,请填充数据。这可以在 Visual Studio 2005 中轻松完成,方法是打开表并进入数据表样式编辑器。
步骤 5:修改 App.Config 文件以包含测试数据库
为了让您的数据驱动单元测试与新创建的测试数据一起工作,您需要在 app.config 文件中指定所有内容。这在两个部分中完成:连接字符串和数据源。下载文件中有一个示例。
- 连接字符串
- DataSources
在 app.config 文件的 <connectionStrings>
部分添加您的刚刚创建的测试数据库的连接字符串。
<add name="TestData" providerName="System.Data.OleDb"
connectionString=
"Provider=Microsoft.SQLSERVER.MOBILE.OLEDB.3.0;Data Source=MyTestData.sdf"/>
请注意,数据库文件名中没有路径信息 - 下一步将详细介绍。
<dataSources>
部分在 <microsoft.visualstudio.testtools>
部分中指定。您可能需要将其添加到您的 app.config 文件中。然后,为测试数据数据库中创建的每个表添加一个数据源定义(这就是我建议您将单元测试与数据库中的表进行一对一映射的原因)。您的 app.config 应该包含类似以下内容(尽管它还会包含其他 Section Groups、sections、connection stings 和其他重要设置 - 不要将其清空并仅放入此示例!)。
<configSections>
<section name="microsoft.visualstudio.testtools"
type="Microsoft.VisualStudio.TestTools.UnitTesting.TestConfigurationSection,
Microsoft.VisualStudio.QualityTools.UnitTestFramework, Version=8.0.0.0,
Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"/>
</configSections>
<microsoft.visualstudio.testtools>
<dataSources>
<add name="WidgetInfoTest" connectionString="TestData" dataTableName="WidgetTest"
dataAccessMethod="Sequential" /></dataSources>
</microsoft.visualstudio.testtools>
步骤 6:将测试数据库包含到测试配置部署中
当您在 Visual Studio 中编译并运行单元测试时,它不会像您可能认为的那样从项目下的 \bin\debug 目录运行。相反,它会被复制到一个名为“TestResults\<Username_ComputerName YYYY-MM-DD HH_MM_SS\out”的目录。现在,由于您想确保拥有最新版本的测试数据,因此需要确保测试数据库就在那里,紧挨着二进制文件,随时可以打开。
要做到这一点,您需要将数据库包含在“localtestrun.testrunconfig”文件中。当您创建新的测试项目时,此文件会自动为您创建,并且它会保存在解决方案级别,在 VS 的“解决方案项”下。
步骤是
- 通过右键单击 localtestrun.testrunconfig 文件并选择“打开”,将其打开。
- 单击列表中的“部署”部分,然后单击“添加文件”。
- 选择您在步骤 2 中创建的数据库文件。它应该会出现在列表中。
更改 testrunconfig 文件
单击“应用”并“关闭”。现在每次运行测试时,都会将测试数据库的副本放在与之一起运行的代码旁边。
如果您担心每次运行测试时都会生成一个小的 .sdf 文件,我建议您
- 购买更多磁盘空间,
- 定期清理,或者
- 使用集中式数据库。
对于集中式数据库,您可以在 app.config 文件中指定完整路径,而无需将其包含在 testrunconfig 部署中。但是,您将失去在任何机器上运行单元测试的能力(除非您在每台机器上设置相同的路径) - 这是否会成为问题将取决于具体情况。
步骤 7:将您的单元测试与数据驱动的马车连接
单元测试的声明是指定其为数据驱动的地方。您只需要指定要在测试中使用的 app.config 文件中的数据源即可。请参见下面的定义。为了好玩,我还添加了一个 Description 声明。
//C#
[TestMethod, DataSource("WidgetInfoTest"), Description("Test out Widget Save")]
public void MyWidgetSaveTest()
{
//test code goes here
}
'VB
<TestMethod, DataSource("WidgetInfoTest"), Description("Test out Widget Save")>()
Public Sub MyDotNetNukeSqlTest()
' Test Code goes here
End Sub
所有数据驱动测试所做的就是打开一个在 DataSource 中指定的游标,然后针对游标中的每一行迭代单元测试。因此,您只需要像在其他任何地方一样访问当前游标行。在这种情况下,它保存在 TestContext.DataRow
属性中(TextContext
属性在第一篇文章的底层 DnnUnitTest
类中定义)。
这是一个“之前”的单元测试方法代码,使用硬编码值来测试 Widget 的保存。请注意,我假设数据库保存中的“失败”以布尔值而不是异常返回 - 取决于代码的编写方式,失败可能是捕获的异常。此外,这里的假设是,当保存 Widget 时,WidgetId
值由存储过程的输出参数设置并与对象一起返回(我个人就是这样做的)。
//C#
[TestMethod, DataSource("WidgetInfoTest"),
Description("Test out Widget Save")]
public void MyWidgetSaveTest()
{
WidgetInfo myWidget = new WidgetInfo();
myWidget.WidgetName = "Test Widget";
myWidget.WidgetPrice = 2.44;
myWidget.WidgetCategoryId = 4;
SqlDataProvider sqlProvider = new SqlDataProvder();
bool retVal = sqlProvider.Save(myWidget);
Assert.IsTrue(retVal);
Assert.AreNotEqual(myWidget.WidgetId, 0);
}
'VB
<TestMethod>()
Public Sub MyDotNetNukeSqlTest()
Dim myWidget as WidgetInfo = new WidgetInfo()
myWidget.WidgetName = "Test Widget"
myWidget.WidgetPrice = 2.44
myWidget.WidgetCategoryId = 4
Dim sqlProvider as SqlDataProvider = new SqlDataProvder()
Dim retVal as Boolean = sqlProvider.Save(myWidget)
Assert.IsTrue(retVal);
Assert.AreNotEqual(myWidget.WidgetId, 0);
End Sub
这是转换为数据驱动之后的单元测试代码
//c#
[TestMethod]
public void MyWidgetSaveTest()
{
WidgetInfo myWidget = new WidgetInfo();
myWidget.WidgetName = (string)TestContext.DataRow["WidgetName"];
myWidget.WidgetPrice = (double)TestContext.DataRow["WidgetPrice"];
myWidget.WidgetCategoryId = (int)TestContext.DataRow["WidgetCategoryId"];
SqlDataProvider sqlProvider = new SqlDataProvder();
bool retVal = sqlProvider.Save(myWidget);
bool expected = (bool)TestContext.DataRow["TestOutcome"];
//check that the return value from the save is what was expected
Assert.AreEqual(retVal, expected);
Assert.AreNotEqual(myWidget.WidgetId, 0);
//store the returned identity value of WidgetId in the TestContext row.
TestContext.DataRow["WidgetId"] = myWidget.WidgetId;
TestContext.DataRow.AcceptChanges(); //saves the test data row
}
'VB
<TestMethod, DataSource("WidgetInfoTest"), Description("Test out Widget Save")>()
Public Sub MyDotNetNukeSqlTest()
Dim myWidget as WidgetInfo = new WidgetInfo();
myWidget.WidgetName = Convert.ToString(TestContext.DataRow["WidgetName"])
myWidget.WidgetPrice = Convert.ToDouble(TestContext.DataRow["WidgetPrice"])
myWidget.WidgetCategoryId = Convert.ToInt32(TestContext.DataRow["WidgetCategoryId"])
Dim sqlProvider as SqlDataProvider = new SqlDataProvider()
Dim retVal as Boolean = sqlProvider.Save(myWidget)
Dim expected as Boolean = Convert.ToBoolean(TestContext.DataRow["TestOutcome"])
'check that the return value from the save is what was expected
Assert.AreEqual(retVal, expected)
Assert.AreNotEqual(myWidget.WidgetId, 0)
'store the returned identity value of WidgetId in the TestContext row.
TestContext.DataRow["WidgetId"] = myWidget.WidgetId
TestContext.DataRow.AcceptChanges() 'saves the test data row
End Sub
如您所见,测试现在从 TestContext.DataRow
游标获取测试数据。而且,由于我们从保存过程中生成 WidgetId,我正在将 Widget 存储回 DataRow
- 这反过来将其存储在 SQL Server CE 表中 - 允许它被其他地方使用。此外,单元测试的通过/失败状态与测试数据一起存储,因此相同的测试会针对正例和负例进行检查 - 这会在与成功案例相同的代码中测试失败处理 - 当在您的 DotNetNuke 模块中“正确”实现时,它很可能会做到这一点。
步骤 8:清理现场
单元测试可能带来的一个主要问题是它可能会堵塞您的数据库,用测试数据填充。这可能是一个问题,也可能不是,这取决于您编写的应用程序类型。我通常的做法是存储一个标志来指示测试运行后如何处理测试数据(无论是成功、失败还是“无论如何”)。
这在我的测试表中作为“DeleteOnComplete”。实际上,您可以定义任何适合您测试需求的列 - 无论是布尔值,“魔术数字”还是文本字符串 - 实际上并不重要。
您可以通过在“TestCleanup
”方法中读取该标志来确定如何操作。您可以访问 SqlDataProvider 中的方法,或者如果您没有合适的删除方法,您可以直接将一些原始 SQL 提交到您的 DotNetNuke 数据库。无论哪种方式都可以。在这里,我只是使用 Microsoft Application Blocks 的 SqlHelper
类,将一些原始 SQL 提交回 DotNetNuke 数据库,如 _sqlProvider.ConnectionString
所指定。
//C#
[TestCleanup,Description("Cleanup for test"), DataSource("TestOrgPerson")]
public void CleanupTestData()
{
bool delete = (bool)TestContext.DataRow["DeleteOnComplete"];
if (delete)
{
int widgetId = (int)TestContext.DataRow["WidgetId"];
SqlDataProvider sqlProvider = new SqlDataProvder();
Microsoft.ApplicationBlocks.Data.SqlHelper.ExecuteNonQuery(sqlProvider.ConnectionString,
"DELETE FROM dnn_Widget where WidgetId = " + widgetId.ToString());
}
}
'VB
<TestCleanup,Description("Cleanup for test"), DataSource("TestOrgPerson")>()
Public Sub CleanupTestData()
Dim delete as Boolean = Convert.ToBoolean(TestContext.DataRow["DeleteOnComplete"])
If delete = True Then
Dim widgetId as Int32 = Convert.ToInt32(TestContext.DataRow["WidgetId"])
Dim sqlProvider as SqlDataProvider = new SqlDataProvider()
Microsoft.ApplicationBlocks.Data.SqlHelper.ExecuteNonQuery(sqlProvider.ConnectionString,_
"DELETE FROM dnn_Widget where WidgetId = " + widgetId.ToString())
End If
End Sub
这段代码将在每次单元测试完成时运行,无论它成功还是失败。通过使用它,您的测试数据将进出您的数据库,而不会用重复的值污染数据(除非您想这样做!)。
将 DotNetNuke 数据库表用作测试数据源
当然,有许多不同类型的测试。让我们假设,例如,您需要测试一个为 dnn_User 表中的每个用户工作的代码片段。与其将所有这些用户放入测试数据数据库表中,不如直接将您的 DotNetNuke 数据库的 dnn_User 表设置为数据驱动测试的数据源。
您只需要这样做:
- 在
<microsoft.visualstudio.testtools>
部分中添加一个新的 DataSource,它看起来像这样: - 在您的
[TestMethod,DataSource("dnnUsers")]
声明中指定“dnnUsers”作为数据源,您将拥有所有用户来迭代和测试您的代码 - 或您选择的任何表。
<add name="dnnUsers" connectionString="SiteSqlServer"
dataTableName="dnn_Users" dataAccessMethod="Sequential"/>
这将根据您在 SiteSqlServer 连接字符串中指定的 dnn_Users 表打开一个仅向前扫描的游标。
您不能同时使用两者(测试数据和“真实”数据),因为它基于单个连接字符串工作,但没有任何东西可以阻止您编写一些单元测试代码来从“真实”数据库中获取记录以进一步细化您的测试输入。一个例子是选择一个特定的 Tab 或 TabModule 记录来处理。这可以通过 SQL 语句或使用内置的 DotNetNuke DataProvider 方法来完成。
对数据驱动单元测试的一些思考
我认为数据驱动单元测试总是值得付出一点额外的努力。事实是,当用户向您的网站添加数据时,您永远不知道他们会往里面添加什么类型的东西。通过一组数据驱动的单元测试,您可以随时向测试数据表中添加更多数据,并查看代码如何处理它。在使用表驱动的方法时,可以更容易地生成大量测试数据。测试的数据越多,总体结果越好。
同样,我总是花时间正确清理,因为数据库中大量通用数据使得发现错误更加困难。当您有 500 条标题为“测试数据”的记录时,您如何知道您的搜索结果是否返回了重复记录?我仔细考虑在测试完成后应该保留什么,应该删除什么。
此外,如果您正在运行一个“构建”服务器以及 TFS 或 NAnt 等产品,您应该认真考虑在每次构建后自动化运行您的单元测试。这就是专业人士的做法,虽然花费多少时间需要权衡,但回报几乎总是值得的,因为您会更早地发现 bug,并且使它们更容易追溯。您应该将 SQL Server CE 数据库签入您的源代码控制系统,以便代码、单元测试代码和单元测试数据都作为单个原子更改集存储。否则,您的测试数据和单元测试代码很快就会不同步,一旦发生这种情况,您很可能会放弃。
这对您有效吗?
与第一篇文章一样,这段代码最初是混乱的实验,我不断地对其进行改进,使其按我想要的方式工作,而本文也源于同一个“备忘单”。我仍在完善我的 DotNetNuke 单元测试方法,但根据第一篇文章的反馈,我认为其他人的想法也在朝着同一个方向发展。在我看来,数据驱动的测试优于静态测试,并且编写起来只比静态测试复杂一点点。
如果您下载了代码,使用了它,并且发现了问题或想出了更好的做事方式,请使用本文的评论区告诉我以及其他人您的体验。
历史
- 2007 年 6 月 18 日 - 初始版本。