65.9K
CodeProject 正在变化。 阅读更多。
Home

单元测试 DotNetNuke 私有程序集模块

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.69/5 (8投票s)

2007年6月7日

CPOL

18分钟阅读

viewsIcon

86586

downloadIcon

737

本文介绍如何创建单元测试来测试 DotNetNuke 自定义模块的代码。

引言

如果您和我一样,您已经尝试了两种不同的事物,并发现您喜欢它们。我说的这两种事物是:

  1. 构建自定义 DotNetNuke 模块,以及
  2. 使用 Microsoft Visual Studio 2005 中的单元测试框架。

然而,就像罐头金枪鱼和菠萝一样*,起初很多人可能会说这两者并不真正相配,但经过进一步的审视,他们会发现,通过一点谨慎的融合,它们确实是互补的。

不适合的主要原因是 DotNetNuke PA 模块开发中通常会涉及到大量的 UI 中心工作。但我认为将单元测试引入 PA 开发恰恰是因为这个原因——在开发中有一个良好的单元测试框架可以确保您不会将 UI 层和数据层过紧地耦合在一起。而使用测试驱动开发 (TDD) 是有回报的,至少对我来说是这样,我喜欢“草拟”代码——尝试几种不同的方法并重构以获得最终产品。我的主要重点始终是让逻辑层脱离 UI 层,以便重用和良好的架构。

本文无意讨论单元测试的优点、理解单元测试或创建单元测试项目。它假定读者:

  1. 理解 Visual Studio 中的单元测试,以及
  2. 理解 DotNetNuke 模块开发。

如果您不理解这两个主题,最好查阅已发布的许多资源。

注意:本文是两部分中的第一部分。第二部分可在 使用数据驱动的单元测试与 DotNetNuke 中找到。第二篇文章展示了如何使用数据驱动的单元测试,但需要先阅读本文才能理解。

TDD + DNN = 好

测试驱动开发对于提高质量是一件好事。像许多事情一样,它感觉违反直觉,并且不会立即带来回报(事实上,在交付开发时间方面,它会产生即时成本)。但是,如果您从事中等复杂度的 DotNetNuke 模块开发,您应该考虑一下。特别是如果您具备 Visual Studio Team Edition 的单元测试能力。它使您作为开发人员能够真正快速地重构代码,从而减少前期进行瀑布式规划并确保设计完美无缺的需求。它是反映真实的开发过程,而不是一种我猜测 100 个 DotNetNuke 开发中有 0 个真正遵循的僵化形式化的版本。谁真正知道您完成编码时模块会是什么样子?一个快乐的黑客式程序员实际上可能比一个厌倦了填写设计文档的程序员更有效率。

在我的情况下,我喜欢自下而上地工作,确保数据库设计正确,然后在此基础上构建应用程序。所以我通常从数据结构开始,然后定义访问方法,然后是运行在其之上的 UI 代码。本文重点介绍开发的那一部分——数据访问代码的定义和开发。如果您想要可重复的端到端测试您的创建/更新/删除 (CRUD) 代码,您需要一套单元测试。正如单元测试的本质一样,要使其正确需要一定程度的黑客攻击,但付出努力,您将拥有一个基线版本的工作代码,您可以随心所欲地更改它,而无需担心破坏一切,并允许您进行适当的重构,而不是小心翼翼地行走在玻璃上,担心会打破什么。这(在我看来——您的结果可能有所不同!)是测试驱动开发的最佳实践。只需更改代码,运行测试,然后看看哪里出错了。回去修复它,重新运行测试,重复直到您获得成功的单元测试运行的魔幻绿色勾。

就在最近,在我开发一个模块的过程中,我决定更改底层数据库设计。我所要做的就是运行单元测试来检查之后一切是否仍然正常工作。经过几次代码调整,我又继续前进。如果没有一系列预先编写的单元测试,我将无法确定是否完全正确,而且我可能会因为害怕进行大量返工而坚持使用旧的(不正确的)设计。

Screenshot - Completed Unit Tests

当一切顺利时,您会得到很多“绿色勾”

本文自然围绕可下载代码展开,当然,但是这里是关于它如何/为何工作的背景,以及如何让您自己的 DotNetNuke 单元测试正常运行。我将假定读者已经学会了如何在 Visual Studio Team System 中使用单元测试项目——否则其他内容将没有意义。如果您,读者,熟悉 NUnit 而不是 VS2005 单元测试框架,您可能会稍微修改一下并以同样的方式使其工作——实际上并没有太大区别。

运行 DotNetNuke 单元测试的主要问题是,核心 DotNetNuke 代码中存在一些假设,即库将在 Web 应用程序的上下文中运行——而单元测试应用程序显然不是。

如何创建 DotNetNuke 模块单元测试

第 0 步:下载并解压安装包

您将在步骤 2 和 3 中使用库和文件。您不需要代码来运行它,它只是为了好奇的人,或者想要进一步扩展它的人。

第 1 步:创建单元测试项目(或打开现有项目)

要运行 DotNetNuke 单元测试,您需要一个单元测试项目。这可以通过“添加新项目”菜单项轻松完成。您可以将其创建在一个新解决方案中,也可以将单元测试项目添加到您现有的 DotNetNuke 解决方案中。

第 2 步:为 DNN 库创建文件夹

在您的单元测试项目的根目录下创建一个 `\bin` 文件夹。这是为了引用所需文件。我通常这样做:

MyModuleProject
\bin (where the DotNetNuke dll's go)
\MyModuleProjectTest
\bin\debug (where the compiled test project will go, not the reference files)

从下载中找到 `iFinity.Dnn.Utilities.dll` 库,并将其本地复制到您的 `\bin` 文件夹中。将所有最新的 DotNetNuke 库复制到 `\bin` 目录中——您基本上需要执行“`copy DotnetNuke*.dll`”。您还需要 `Microsoft.ApplicationBlocks.Data.dll`。如果您有这些 DLL 的 `.pdb` 文件,也请将它们复制到目录中;这有助于调试,因为您可以单步进入核心框架代码。

`iFinity.Dnn.Utiltities.dll` DLL 要求 DotNetNuke 库位于同一目录中。下载中没有副本,因为大多数人运行的版本略有不同。

第 3 步:添加项目引用

复制所有 DLL 后,使用“项目-> 添加引用”菜单项,引用该文件和一些 DotNetNuke 库。这些是您的单元测试项目所需的最低限度的库:

  • DotNetNuke.dll
  • DotNetNuke.SqlDataProvider.dll
  • DotNetNuke.Caching.BroadcastPollingCachingProvider.dll
  • DotNetNuke.Caching.FileBasedCachingProvider.dll
  • DotNetNuke.Membership.DataProvider.dll
  • DotNetNuke.Membership.SqlDataProvider.dll
  • DotNetNuke.Provider.AspnetProvider.dll
  • Microsoft.ApplicationBlocks.Data.dll
  • YourNewModule.dll(您正在处理的 PA 模块——如果您在模块解决方案中有测试项目,最好将其作为项目引用而不是文件引用)。

此列表实际上取决于您将在单元测试中执行哪些测试——可能需要仔细检查,直到所有正确的库都已引用。

第 4 步:创建 app.config 文件

在您的单元测试项目中创建一个 `app.config` 文件。您可以使用“项目-> 添加新项”菜单命令创建一个,然后选择“应用程序配置文件”作为模板类型。或者,只需创建一个 XML 文件并将其重命名为 `app.config`——无论哪种方式都可以。

从您的 DotNetNuke `web.config` 文件中,复制适用于连接字符串、缓存提供程序和 SqlDataProviders 的 DotNetNuke 部分。Visual Studio 单元测试框架将自动检测并使用 `app.config` 设置,因此当 DotNetNuke 框架代码查找如缓存提供程序和连接字符串等配置设置时,它将从 `app.config` 文件中获取,而不是在网站目录中查找 `web.config` 文件。

您还需要“iFinity”sectionGroup 才能运行提供的代码。这是因为您将使用的基类需要一些数据来初始化。做到这一切最简单的方法就是复制可下载代码中的 `app.config` 文件。它具有简单的单元测试应用程序的所有正确设置。

您可能需要复制更多的提供程序设置,具体取决于您在基础框架中访问的提供程序。如果您开始在 DotNetNuke 库的深处进行单元测试,不要惊讶地发现您需要引用一个当前不在您的 `app.config` 文件中的提供程序。这可能会表现为一系列空引用异常或类似难以找到的问题。提供的示例涵盖了大多数问题,但对于那些推向极限或使用自定义提供程序的人来说,您可能会遇到麻烦。

第 4 步:创建新的单元测试

您现在可以创建一个 DotNetNuke 单元测试了。创建一个新的单元测试类并为其命名(以及命名空间)。

在您的 `UnitTest` 类文件中,您需要添加对正确库的引用,因此使用“`Imports`”(VB)或“`using`”(C#)命令引用它们。

  • DotNetNuke.Data
  • iFinity.DNN.Utilities.Testing
  • YourCompany.YourModule(或您的 `SqlDataProvider` 和 `*Info` 类所在的任何命名空间)

然后,更改类的声明,使其继承自 `DnnUnitTest` 类型。基类会在后台处理所有 DotNetNuke 的相关操作,因此您无需手动操作。

//C#
public class MyDotNetNukeClassTest : DnnUnitTest
VB
Public Class MyDotNetNukeClassTest Inherits DnnUnitTest

底层基类有一个强制性的构造函数,所以您需要创建它。它需要一个 portal ID,所以我通常只是硬编码我正在使用的测试数据库上的 portalID。如果您想尝试另一个 portal,可以随时更改它,或者您可以想出一个巧妙的解决方案来替换 portal ID。

//C#
public MyDotNetNukeClassTest ():base(1) //send PortalId to base class
{}
VB
Public Sub New()
  MyBase.New(1) 'Send Portal ID to base class
End Sub

第 6 步:创建新的测试

现在,使用您通常会使用的 `[TestMethod]` 符号在您的代码中创建一个单元测试。然后,像往常一样编写您的 DotNetNuke 代码的单元测试。我通常直接在代码中创建我的 `SqlDataProvider` 对象并对其进行测试,而不是通过创建 `DataProvider` 类并通过 `Instance()` 调用来间接处理。

一个示例测试方法是

//C#
[TestMethod]
public void MyDotNetNukeSqlTest()
{
    SqlDataProvider sqlProvider = new SqlDataProvder();
    MyThingInfo myThing  = new MyThingInfo();
    myThing.Property1  = "property1";
    myThing.Property2 = "property2";
    sqlProvider.SaveMyThing(myThing);
    copyOfmyThing = sqlProvider.GetMyThing();
    Assert.AreEqual(myThing.Property1, copyOfMyThing.Property1);
    Assert.AreEqual(myThing.Property2, copyOfMyThing.Property2);
}
VB
<TestMethod>()
Public Sub MyDotNetNukeSqlTest()
    Dim sqlProvider as SqlDataProvider = new SqlDataProvder()
    Dim myThing as MyThingInfo  = new MyThingInfo()
    myThing.Property1  = "property1"
    myThing.Property2 = "property2"
    sqlProvider.SaveMyThing(myThing)
    copyOfmyThing = sqlProvider.GetMyThing()
    Assert.AreEqual(myThing.Property1, copyOfMyThing.Property1)
    Assert.AreEqual(myThing.Property2, copyOfMyThing.Property2)
End Sub

如您所见,我的示例测试方法只是将一个 `MyThing` 保存起来,然后将其读回来,然后比较这两个属性以确保它们具有相同的值。您需要考虑您的 PA 模块代码的哪些方面您想要测试,并相应地编写单元测试。

如果运气好并且一切都做对了,您应该可以直接运行单元测试并得到一个绿色勾。如果不行,那么将需要一些故障排除来摆脱那些讨厌的红色叉。您应该能够单步进入您的代码,看看哪里出错了。

故障排除

Screenshot - Failed Unit Tests

当事情不顺利时,可怕的红色叉

一个常见问题是命名空间/程序集名称不正确——在尝试开始单元测试之前,确保您的 `SqlDataProvider` 命名空间和程序集名称正确非常重要。DotNetNuke 使用一个反射程序集,该程序集对命名标准做出某些假设,因此坚持这些标准很重要,否则您将花费数小时分析代码,弄清楚为什么您的数据提供程序无法实例化。您需要确保您的 `SqlDataProvider` 的程序集名称和命名空间匹配。下面是我一个项目的示例:

  • 完整类型名称: iFinity.DotNetNuke.Modules.Directory.Data.SqlDataProvider
  • 程序集名称: iFinity.DotNetNuke.Modules.Directory.SqlDataProvider.dll

这将在您的 DataProvider 模块中引用,最有可能。

如果您没有在测试项目中正确引用所有 DotNetNuke 库,您将遇到类似这样的错误:

Unable to create instance of class iFinity.Tagger.Test.TagInfoSQLTest. Error: 
System.TypeInitializationException: The type initializer 
for 'DotNetNuke.Services.Cache.CachingProvider' 
threw an exception. ---> System.ArgumentNullException: Value cannot be null.

Parameter name: type.

此特定错误是因为缓存提供程序在 `app.config` 文件中被引用,但在项目引用中未被引用。事实上,您很幸运能得到一个如此清晰的错误。

在尝试使其工作时,最好在异常抛出时启用异常捕获,这样您就可以在异常发生时立即捕获它们。否则,您将面临大量的“空引用异常”,因为对象无法正确实例化。您需要仔细检查堆栈跟踪和异常堆栈——问题通常是调用链深处的一个找不到的文件,需要坚持不懈和注重细节的工作才能正确处理。

请记住,一旦您解决了问题,请将异常处理设置回“仅未处理”,否则您将追逐许多暗道,在这些地方的代码是为了使用 `try..catch` 块来处理正常操作(这是性能上的禁忌,但有时会被使用)。DotNetNuke 中有一些这样的代码,特别是在 `CBO` 命名空间中(这也是为什么它应该被避免,并且在我看来,应该用直接转换的数据库例程来代替)。

编写所有必需的单元测试

一旦您完成了一个单元测试,就可以开始编写其余的了。根据您遵循的模式,您很可能至少有四个操作需要测试(CRUD 操作)。假设一个名为 '`MyThings`' 的模块,其 `MyThingInfo` 类,您的 `DataProvider` 将有一系列方法,如下所示:

  • AddMyThing
  • UpdateMyThing
  • GetMyThing
  • DeleteMyThing

应该为其中每一个编写一个单元测试(`AddMyThingTest`、`UpdateMyThingTest` 等)。但不要忘记单元测试是为了测试,所以您需要使用 `GetMyThing` 调用来检索您保存的记录。然后,您应该检查所有属性是否都已正确返回。您还应该测试诸如添加重复键之类的事情;当您传递无效数据时会发生什么——而不仅仅是“积极”的测试用例。并且不要忘记在测试结束时删除您的测试数据,除非您希望您的数据库被测试数据塞满。

当然,在实践中,我还没有创建过一个具有如此简单性的 DotNetNuke 模块,但您永远不知道,但它有助于说明这个概念。我目前正在开发的模块大约有 25 个不同的数据访问方法,每个方法都有一个或两个单元测试。

它是如何工作的?

如前所述,将单元测试与 DotNetNuke 混合的根本困难在于代码将在 Web 应用程序内部运行的假设。真正的棘手之处在于 `HttpContext` 被用于缓存项目,以及各种 DotNetNuke 缓存提供程序。现在,DotNetNuke 代码足够健壮,可以在缓存中找不到对象时创建对象,并在您请求一个对象时保证您获得一个已实例化的对象,但它确实期望缓存首先存在。没有 Web 应用程序,就没有 `HttpContext`,您就陷入了困境。

你在嘲讽我吗?

模拟是模拟单元测试时后端功能缺失的过程。也称为“存根”和其他名称,它基本上是提供一个供代码在测试上下文中运行时调用的东西,而不是完整的 DotNetNuke 应用程序。很快您就会调用类似 `HttpContext` 的东西,并发现单元测试上下文没有运行这样的东西。因此,您需要模拟 DotNetNuke 理所当然的一些东西。我们可以稍后讨论设计架构和库与网站的分离。我从不批评这个框架,因为它是在自愿的基础上完成的,并且是免费分发的。

要查看模拟 HTTP 上下文的简单方法,请参见以下内容:在不使用 Cassini 或 IIS 的情况下模拟用于单元测试的 HttpContext

使用 Cassini 的更全面的方法(更重但更完整)是使用 Cassini (ASP.NET Web Matrix/Visual Studio Web Developer) 进行 ASP.NET 页面、基类、控件和其他小部件的 NUnit 单元测试

我实现了第一个链接中使用的某种方法。这为 DotNetNuke 库提供了运行所需的正确对象。

对于那些懒得阅读(RTA)的人来说,它所做的是设置一个简单的对象来模拟 `HttpContext` 的一些功能。不是全部,所以您不需要强迫它走很远就能打破它(它不支持对 `HttpContext` 的绝大多数调用)。但您正在构建类库!您为什么要假设会有 HTTP 上下文?

我能把我的东西放在哪里?

DotNetNuke 在运行时大量使用缓存来存储各种东西。其中一些是提供者的对象(取决于您是否使用磁盘缓存)。如果没有地方存放所有这些实例化的提供程序,您将到处都会出现异常。因此,您需要确保设置了缓存路径。不,没有一个神奇的“`SetMyCachePathAndMakeMyProblemsGoAway`”方法可以调用。您必须想办法欺骗 DotNetNuke 库,让它以为它实际上正在正常运行。为此,必须设置一些“启动”变量。

在我的基类中,这些路径被设置为 `app.config` 文件中指定的那些值。我自己不太喜欢这种方法,因为它意味着运行您的单元测试的任何其他机器(例如,构建机器)都需要指定相同的路径。我不太成功地让相对路径正常工作,尽管我愿意承认它可能并不那么难。

如果您查看 iFinity/DnnUnitTest 设置,您会看到几个指定的路径:

<DnnUnitTest hostMapPath="C:\DotNetNuke\source\DotNetNuke451\Website\portals\1\" 
  appPath="C:\DotNetNuke\source\DotNetNuke451\Website\" appName="DotNetNuke" 
  simulatedServer="WOMBAT" simulatedPage="default.aspx"/> 

这些是:

  • `hostMapPath`:这是应用程序正常运行时缓存数据等的位置。在这种情况下,我已将路径设置为我的本地 DotNetNuke 安装所在的位置。
  • `appPath`:网站正常运行的根路径。同样,我已经将其设置为我的本地安装。
  • `simulatedServer`:据我所知,这个值并不重要,但需要指定。我使用我的本地机器名;“localhost”可能也可以。
  • `simulatedPage`:您正在模拟的 ASP.NET 页面的名称。因为所有 DotNetNuke 页面都运行为 `default.aspx`,所以这个很容易。

注意:您不必将它们设置为与本地 DotNetNuke 安装相同的值,并且您实际上不需要在您使用的计算机上安装完整的 DotNetNuke 网站。只要它们是有效的路径,一切都会没问题。我只使用我的本地 DotNetNuke 安装,因为文件夹已经创建并具有正确的权限。

这可以推到多远?

最终,您会发现自己正在尝试单元测试半个 DotNetNuke 网站,或者您会发现自己正在疯狂地模拟无法也不会被模拟的东西。然而,通过一些创造性的编码和深入核心源代码,您会发现大多数问题都可以克服。然后,您将拥有一套可靠的单元测试,每次更改代码时都可以运行。任何中断都会很快被发现,当情况需要时,您会自信地进行真正的重构。

不仅如此,当您开始在 ASCX 文件中编写 UI 代码时,您将对数据层代码如您所愿地正常工作充满信心。调试 Web 应用程序是一件很痛苦的事情,所以最好在将其连接到 Web 应用程序之前进行调试。

成功了吗?

代码最初是我不断完善的助手,直到它能做到我想要的,而本文最初是我的“备忘单”,以帮助我更快地进行故障排除。它最终演变成供公众消费的东西,但其中可能存在我没有发现的模糊说明和问题。如果您使用它并且在使用过程中遇到麻烦或不理解它,请使用下面的评论字段提问,我会尽力澄清任何困难。

* 在上大学期间,我曾短暂地与一位苏格兰健美运动员合租过房子,他是一名保镖。他每天午餐都吃金枪鱼和菠萝,因为它们富含蛋白质和糖,脂肪含量低。哦,而且两罐大约只需要 1 美元。它是咸味和甜味的混合,干燥和多汁的质地使其与众不同。比泡面(又名拉面)好多了,因为不用煮,而且营养价值更高。

历史

  • 初始版本:2007 年 6 月 8 日
  • 已更新数据驱动的文章链接:2007 年 6 月 18 日
© . All rights reserved.