UML软件工程组织

强化Visual Studio 单元测试
 来源:MSDN http://www.csai.cn

密切注意Hardcoded路径

 使用单元测试向导在一个新的项目上添加测试是一件非常容易的事情。这个便利的特点可以节省你数百个小时的打字时间(微软的开发者为此应收取很多费用)。但是,一些事情在幕后发生了,并且导致你很悲伤:路径是hardcoded!这是一个问题,例如,当你移动一个测试到另一台机器或是目录时就会发生。我希望一个Visual Studio 2005的Service Pack允许你为测试设置一个开始路径,hardcoded路径将使用相对路径。目前,但是解决hardcoded路径的容易方式就是在团队中有一个慈善的独裁者,它总是判断和颁布所有的驱动和路径。如果它是一个小的项目,这种解决方案是很好的。另一个,一些实用的工作区在你的路径中将引用% testdeploymentdir%环境变量。当测试运行时它被设置。

 Hardcoded路径出现的第一个地方就是在VSMDI文件中,这个文件是一个大的包装皮里面包含简单的测试列表。当你打开一个VSMDI文件,然后它不能找到测试集或者TESTRUNCONFIG文件,你将被提示为这些项目指出位置。我发现有意思的事情是下一次我再打开相同的VSMDI文件时,它会发现所有的东西。很明显,更新的路径一定存储再某个地方,但是VSMDI文件本身并没有被改变。我再VSMDI文件驻留的目录中发现一个隐藏文件,名称为 name<filename>.VSMDI.OPTIONS。

 当我打开VSMDI.OPTIONS文件时(它是一个常见的XML文件),我摇晃我的头,感觉受挫一样。就像你在 Figure 1中看到的一样,在VSMDI文件中很明显的有路径搜索的支持,但是在Visual Studio用户界面中没有一种方式可以设置搜索的路径。(此外,对这些VSMDI文件来说没有理由被隐藏啊。)所以,当我想在一个不同的目录结构中使用 VSMDI文件时,首先复制一个示例的VSMDI.OPTIONS文件到适当的目录,然后手动的编辑它,在程序集和TESTRUNCONFIG文件中添加路径。为这一部分的源代码,我已经在BaseVSMDIOPTIONSFile文件夹中示例出了一个文件。

 使用VSMDI文件主要的原因就是你可以指定要运行的测试列表-在动态开发过程中它是非常有用的,因为它允许你运行这些具体的测试,这些测试与你工作使用的代码相关。在一个测试集中要运行所有的测试,你可以使用控制台测试运行器,MSTEST.EXE。在这有大量的命令行选项,但是你仅仅需要” /testcontainer”选项,它指定了包含在测试中的程序集。另外,你可以使用”/resultfile”来指定结果文件的名称。当使用 MSTEST.EXE时,你不必担心VSMDI文件。在下一部分,我将论述一个MSBuild.EXE任务,它可以自动的运行在目录结构中发现的所有测试,所以你可以避免和VSMDI文件一起使用。

 你发现hardcoded路径的第二个位置就是在TESTRUNCONFIG文件中;指向instrument和 strong key文件的程序集就包含hardcoded路径。我曾经指出了解决这个问题最好的办法就是当你在不同的目录结构下运行测试时使用一个不同的 TESTRUNCONFIG文件。因为没有办法创建一个新的TESTRUNCONFIG文件,你需要复制一个已经存在的文件到机器位置,然后使用 Visual Studio编辑它。如果驱动和上一级目录不同,IDE和MSTEST.EXE讲使用相关的路径来处理TESTRUNCONFIG文件,但是,你必须手动的编辑它们。

 因为它本身不是做代码覆盖的测试工具,所以没有办法阻止为来自命令行的已经编译的程序集手动的执行代码覆盖。如果你使用ASP.NET进行工作,你需要从Visual Studio集成开发环境中做代码覆盖来创建合适的工具集,然后使它成为工具。

 手动的做代码覆盖是一个三步的过程。第一步就是实现程序集以致你在它们中拥有代码覆盖钩(hooks)。第二步就是开始监视进程,然后告诉它在哪编写覆盖文件。任何在监视进程运行时加载的已经实现的二进制将它们的覆盖数据添加到输出文件。最后一步就是关闭监视,编写 CONVERAGE文件。我已经制作出一个Converage.Targets文件,你可以和MSBuild一起使用来自动化这个过程。Figure 2显示出一些文件;您可以从MSDN?Magazine网站下载到全部的文件。

 当你浏览Figure 2时,你将看到在Bugslayer.Build.Task.DLL中我写了一个任务来运行监视进程。Figure 3在Visual Studio中显示了它。

 当首次编写Civerage.TARGETS时,我使用Exec任务执行VSPERFCMD /START:Coverage /OUTPUT:$(OutputCoverageFile),但是在做这时有一个严重的错误,MSBulid.EXE在调用时完全的挂起了。 VSPERFCMD.EXE产生出了真实的监视进程,VSPERFMON.EXE。如果你在命令提示符下运行VSPERFMON.EXE,进程将待在那吐出连接和进程中其它活动的信息,所以你不能从项目文件中直接的调用它。

 这个问题出现在MSBuild.EXE中,滋生于某个事件,这个事件就是VSPERFMON.EXE从带有 bInheritHandles标记的 VSPERFCMD.EXE进程中产生到CreateProcess,并且设置为“真”。任何带有继承句柄开始的进程将在MSBuild.EXE下挂起。因此,我必须在任务中从ITask.Execute方法调用Process.Start,通过这样操作才能使MSBuild.EXE下的所有事情正常运行。

与GenericTest和EXEs一起合作

 如果你已经获得一个基于EXE程序的存在的测试系统,在文档中GenericTest类型的论述可能伤害您的好奇心。当在一个依赖运行九个EXE作为单元测试的批处理文件的项目上进行工作时,我可以使用GenericTest类型快速的包装一些自动化过程中存在的代码。虽然这也有一些catch。第一个小的障碍就是GenericTest允许0作为一个成功的来自EXE的返回值。那并不是一个很大的处理,但是考虑到 GenericTest的高级特性,我很沮丧的看到与可接受的退出代码区域一样简单的事情被遗漏了。

 GenericTest的一个比较大的问题就是它是hardcoding的一个堡垒。幸运地,可以容易地指出相对路径地文治。如果你地 GenericTest存在C:\FOO中,事实上测试将从C:\FOO\TestResults\<User>_< Machine>_<TimeStamp>\Out开始。这样,如果被GenericTest执行地EXE文件在C:\FOO中,你可以使用./././<name>.EXE作为程序来执行。不幸地是,几乎GenericTest中地其他事情从驱动器中被硬编码。有趣地是外部目录就是你地二进制每次运行时被复制到地地方。即使你改变了代码,你可以重新运行测试地以前版本以重新生成问题。

 一个便利地特性,GenericTest类型将捕获定位到标准输出地任何事情,在结果文件中提供一个运行日志。不幸的是,这看上去像是一个捕获的问题,在那提取一些信息将导致测试驱动进程挂起。但是大部分测试程序在几秒中内不会抽空输出结果中的100行。

创建单元测试

 当提到测试,Visual Studio真正的魔力就是当你在编辑器里右击一个方法时,它可以奇妙的创建你得到的单元测试选项。这个特性非常好,可以很容易的快速添加单元测试。但是,我遇到了一个小的哲学问题,它让你创建可以直接进入类并且访问私有方法的单元测试。

 针对允许测试工具直接调用私有的或受保护的方法的争论就是它减化了测试(只写很少的代码),并且帮助扩宽了代码覆盖。这些争论是很诱人的,但是我同意这个观点,就是认为单元测试应该仅仅通过公共接口出现。单元测试是代码的首次使用,你想朝着其他的怎么使用它的方向来调整测试。如果有一些私有方法,这些方法你在没有short circuiting和直接的调用它们的情况下不能充分的测试,我必须知道是否代码需要被注册。为了阻止偶然的创建一个直接调用私有方法的单元测试,找到创建单元测试对话框,在右上角点击过滤,然后清空显示非公共项目。

 我决不是一个绝对论者。我确定有一些示例,在示例里面它将帮助调用私有方法。但是,仅因为工具允许你做一些事情并不意味着你将依赖它。单元测试是测试的第一阶段,它也是你开始white box测试的第一个位置。

使用NUnit

 我有一些项目,在项目中我们已经在一个扩充NUint的测试系统做出了很大投资。(对.NET Framework 2.0起作用的新版本即将被发布)在一个示例中,我们想要代码在NUnit和Visual Studio测试系统之间是便携式的。当计划这个时,我偶然发现很酷的一些事情,那就是需要最少的代码改变,并且允许代码与NUnit和Visual Studio一起工作。

 我有一些项目,在项目中我们已经在一个扩充NUint的测试系统做出了很大投资。(对.NET Framework 2.0起作用的新版本即将被发布)在一个示例中,我们想要代码在NUnit和Visual Studio测试系统之间是便携式的。当计划这个时,我偶然发现很酷的一些事情,那就是需要最少的代码改变,并且允许代码与NUnit和Visual Studio一起工作。

#if !NUNIT
using Microsoft.VisualStudio.TestTools.UnitTesting;
#else
using NUnit.Framework;
using TestClass = NUnit.Framework.TestFixtureAttribute;
using TestMethod = NUnit.Framework.TestAttribute;
using TestInitialize = NUnit.Framework.SetUpAttribute;
using TestCleanup = NUnit.Framework.TearDownAttribute;
#endif

 我所需要做的就是使用NUnit测试属性改变我的方法为TestMethod,然后我就有一个在两种方式下工作的测试代码。

TimeOutAttribute

 包含决大部分属性的文档固然非常好。但是,最重要的属性之一,TimeOutAttribute并没有包含在API 文档中。当TESTRUNCONFIG 文件允许你指定单元测试的全部超时值时,TimeOutAttribute让你指定一个单独的测试可能花费的最大毫秒数。我发现 TimeOutAttribute在这些联系数据库的测试方法上没有价值,所以我密切注视这些查询。请注意,时间值包含一些测试运行器的时间。另外,机器的速度和性能将影响时间。使用你的测试进行实验来看一下时间选择是怎么在你的系统上工作的。

 TestContext类我们只是简要的涉及到,它也是被单元测试向导添加进来的TextContext属性。主要的论述是关于当你正在使用 DataSourceAttribute时通过使用TestContext类获得数据行。TestContext类有很多内容提供。文档显示出 TestContext类标记为抽象,但是事实上支持你的单元测试的源类型是UnitTestAdapterContext,它来自于 Microsoft.VisualStudio.QualityTools.Tips.UnitTest.Adapter.dl,你可以在< Visual Studio .NET install dir>\Common7\IDE\PrivateAssemblies 中发现这个DLL文件。你可以使用.NET Reflector查看UnitTestAdapterContext以此明白它是如何工作的。

 或许这个类支持的最重要的方法就是WriteLine,你可以使用它添加额外的输出到单独的测试结果中。所有的编写都将出现在报告的额外信息部分。为了发现什么测试正在运行或测试开始于什么目录,你可以单个使用TestContext属性域TestName和 TestDir。最后,如果你想为所有或部分测试设置时间器,调用TestContext.BeginTimer和 TestContext.EndTimer。在标准的控制台输出部分,时间数据将显示在测试运行结果中。

为你写代码

 我曾经提到,我已经包含了一个连同它的单元测试一起的真正单元。Bugslayer.Utility.DLL是一个我已经在项目之间拖拽的有用代码集合。 ArgParser类是一个命令行论据分析类,这个类是基于在旧的.NET Framework SDK WordCount例子中的类。SystemMenuForm类是一个Windows窗体,这个窗体允许你在系统菜单中附加项目和作为常规时间回应点击操作。

 我之所以编写GlobalMessageBox是因为每次使用一个消息框时我非常厌烦看到代码分析错误, Specify MessageBoxOptions。规则规定当使用一个消息框时,你需要浏览类,然后明确是否RightToLeft属性被设置为Yes。如果是,你必须调用合适的MessageBox.Show负载在MessageBoxOptions标记里通过。GlobalMessageBox为了我而关心这个,抑制错误和允许我的代码在右到左的语言系统中正确的工作。

 对于Bugslayer.Utility.DLL的完全单元测试位于Bugslayer.Utility\ Tests\ Bugslayer.Utility.Tests目录中。在各种各样的CS文件中包括39个测试,提供了超过92%的代码覆盖。由于这是一个 Windows 窗体的单元,此单元提出了消息框和控件,你不能够无人参与的运行它,但是它能够在15秒之内运行。

 对于Bugslayer.Utility.DLL的完全单元测试位于Bugslayer.Utility\ Tests\ Bugslayer.Utility.Tests目录中。在各种各样的CS文件中包括39个测试,提供了超过92%的代码覆盖。由于这是一个 Windows 窗体的单元,此单元提出了消息框和控件,你不能够无人参与的运行它,但是它能够在15秒之内运行。

名称和位置

 MSTEST.EXE最不成对的部分就是它的输出命名规范。如果你使用/RUNCONFIG选项来操作一个 TESTRUNCONFIG文件,输出文件将使用在那个文件中规定的命名规范。如果你不使用/RUNCONFIG,或者设置为默认,所有的输出被写到.\ TestResults\< user>_<machine> <timestamp>。我建议使用能够快速识别的名称。

 MSTEST.EXE提供了/RESULTSFILE选项,但是这将导致输出文件名称丢失时间戳。另外,如果指定到 /RESULTSFILE的文件名称退出,MSTEST.EXE将会失败。我所希望的就是指定一个我所集中工作的细节的名称,但是不需要手动的添加时间戳。

 你可能会想可能的解决方法就是使用VSMDI测试源数据文件,这个文件你过去在测试管理器窗口中看到。事实上,MSTEST.EXE没有/TESTMETADATA选项来加载和运行测试。问题是你仅仅能指定一个VSMDI文件。

 一个可能的解决方案就是创建一个单独的VSMDI文件,这个文件在你的代码里面导入所有其它的VSMDI文件。那的确可以工作,但是它也呈现出另外一个维护任务来回忆每一次你添加新的测试到代码中。

 值得注意的是当运行VSMDI文件时你不能够告诉IDE或是MSTEST.EXE将输出文件放在什么位置。输出文件指向了VSMDI文件贮存的目录。建议在一个目录中保存测试,这个目录在版本控制里源代码之下,以致如果你共享项目,所有的测试代码将会跟随它。

 由于VSMDI文件作为每一次测试的一部分,并且没有一种方式来集中输出,输出将围绕着你的源代码被分散。这并不是一个很大的处理,但是它意味着你必须手动的整理源树。在处理一些测试运行结果之后,我想要一种简单的方法来处理这个。

一个更好的MSTEST.EXE

 基于此次论述,我明白了有四个特性我想添加到MSTEST.EXE。第一个是一种方法用来动态的发现所有的单元测试,然后就像小的smoke测试一样运行它们。第二个是一个简单的方法用来识别出相似的测试运行而不仅仅是读时间戳。第三个就是确保所有的测试输出定位到一个单一的位置。最后,我想要一种非常容易的方法来除去无关的测试运行而不管它们在源树中的什么位置。

 这些需求强烈要求MSBuild。Bugslayer.Build.Tests.DLL中的MSTestTask包装MSTEST.EXE所以你能够获得所有的控制。就像你看到的代码,你将注意到它是从ToolTask类获得的,并且是由 Microsoft.Build.Utilities.DLL程序集产生的。当编写一个包装了一个命令行工具(因为它做了大部分的提高)build任务时,这个任务,ToolTask就是你想要使用的。

 对于一些工具,你所需要做的就是定义你唯一的属性,重载三个方法和一个属性。属性是ToolName,它返回了工具的可执行名称。 GenerateFullPathToTool方法返回了完全的驱动,路径和文件名称到工具本身。为了验证这些参数,你需要重载 ToolTask.ValidateParameters方法,如果一切正常的话,返回值为真。为了编译真实的命令行到工具中,重载 ToolTask.GenerateCommandLineCommands然后使用CommandLineBuilder类或者是我对它做的简单的扩展 ExtendedCommandLineBuilder。

 为所有可能的命令行参数运行MSTEST.EXE/?。当它指定输出文件名称的时候需要ResultsFile。你也需要设置TestMetaData参数或者TestContainer参数以此分别的显示出源数据文件或者测试容器。

 为所有可能的命令行参数运行MSTEST.EXE/?。当它指定输出文件名称的时候需要ResultsFile。你也需要设置TestMetaData参数或者TestContainer参数以此分别的显示出源数据文件或者测试容器。

 但是,我对于MSTestTask的长期计划就是扩展测试属性以允许通配符能够为测试名称的执行被通过。那将允许你容易的执行仅符合具体前缀的那些测试。借助在TestContainer属性中被通过的程序集,这工作将仅仅是一系列的反射,查找拥有 TestClassAttribute的类库和一些符合规范表达的带有TestMethodAttribute的方法。

 MSTestTask其他的操作部分来自于RunTests.Targets文件,你可以在.\Build包含源代码的目录中找到这个文件。它包括了非常酷的ExecuteAllTests对象,这个对象在你指定的目录中开始,在整个结构中查找所有的单元测试, GenericTests,WebTests 和OrderedTests,然后执行它们。你可以认为ExecuteAllTests对象对于单元测试是一个来说自动衰退测试。当你添加新的测试时,它将自动的执行它们。在代码下载中包含的RunTests.Targets代码非常明智的使用排除文件来获得我们希望的东西。为了查看一个使用中的 RunTests.Targets例子,查看一下SmokeTest.proj,它为所有的代码显示出smoke测试。

 在.\Build目录中最后的TARGETS文件是MSTestCleanUp.Targets。就像它名称所暗示的那样,它的职责就是查找包含 TestResults的所有目录作为一个路径然后删除它们。它是使用转换的一个很好的例子,就像MSBuild中的RemoveDuplicates任务一样。使用MSTestClean-Up.Targets,你就不用怕其他的文件毁坏你的源目录。

掩饰,包装

 如果你不说,我将对Visual Studio 2005中新的单元测试工具非常兴奋。像ASP.NET和DataGrids这样的程序在此书中受到了所有的关注,但是当你努力按时获得程序时,测试工具将会带来很大的影响。我可以保证,你在使用测试工具上花费的时间越多,你的代码将会变的更好。

 Tip 73 你可以控制一个单元测试默认的编程语言和当一个新的单元测试被单元测试向导创建后哪些条目能被添加到里面。来到选项对话框,扩展测试工具节点,然后转到测试项目属性页面。在那,你将看到默认的测试项目类型combobox和每个语言类型的默认文件选择。如果你像我一样,在你创建了第二个单元测试后你将清除 “About Test Projects”介绍文件。

 Tip 74 试图将一个程序集中的所有的单元测试保存到一个单一的测试程序集。那个一对一的映射来自于维护的观点。但是,当程序集增长时,测试数量将会变得非常大。我喜欢将前缀放到测试方法的名称上,因为这个它们测试的特性有关。这样可以很容易的分组。例如,在Bugslayer.Utility.Tests.DLL 程序集中,关于GlobalMessageBox类的测试开始于”GNB_”

 

版权所有:UML软件工程组织