(翻译)LearnVSXNow! #11- 测试package
在前面的文章里,当我们创建package的时候,总是在向导那里勾掉测试选项,从而不采用自动测试的方式,只是手动的测试它:把package运行起来,并且看看它是否有我们预期的功能。
我之所以勾掉测试选项,并不是因为我讨厌测试,而是我认为package的测试应该作为一个主题单独来讲,需要至少一篇专门的文章来叙述它。坦率的讲,当我在第10篇里写VsxTools的代码的时候,我真的觉得我们缺少了测试这一步:我写了几百行代码了,如何保证它的正确性?
关于测试,有很多很好的书和文章,所以在这篇文章里我不打算再写一篇类似的文章。我虽然是一个测试驱动开发的粉丝,但并不是一个测试专家。所以在这篇文章里我只是讲一些和VSPackage测试相关的基本问题。
我相信VSPackage的自动化测试能带给我们很大的帮助。但我并不会很深入的讲解VSPackage的测试方法,我只是给你们展示一下如何进行VSPackage的测试,这样你们就可以利用VSPackage的测试来帮助你们测试package的正确性。
所以,我假定你们已经了解了单元测试的基本知识,并且知道如何利用Visual Studio进行单元测试。如果你觉得自己还不了解这些,请访问MSDN,你一定会从上面找到相关的信息。
Visual Studio 2005和2008的Team System有专门相对于测试人员的版本。幸运的是,就算是Visual Studio 2008的专业版,也包含了基本的测试功能,所以用专业版已经可以达到我们的测试目的了。
创建一个带测试项目的package
像以前一样,我们会创建一个新的package,但这一次我们会利用VSPackage向导帮我们生成测试项目。好,让我们创建一个名为SimpleTesting的package,当VSPackage向导出来的时候,选择C#作为package的开发语言,然后根据下图的内容填入这个package的基本信息:
在下一步里勾上Menu Command和Tool Window:
设置Menu Command的选项,如下图:
设置Tool Window的选项,如下图:
和我们以前创建package时不一样,这次我们选中这两个测试项目:
这一次,VSPackage向导会创建三个项目。第一个——叫做SimpleTesting——是package项目,另外两个SimpleTesting_IntegrationTestProject和SimpleTesting_UnitTestProject是测试项目。每一个测试项目包含了一些预创建的测试用例:
点击菜单“测试|窗口|测试视图”,可以看到这些测试用例:
修复一个bug先
这个时候我们已经可以开始我们的测试了,你可以点击“测试|运行|解决方案中的所有测试”菜单或者按快捷键Ctrl + R + A来运行所有测试。当测试运行起来之后,Visual Studio启动了一个新实例(当然是VS实验室),并且有一个看不见的东东在操作它。但是,当测试用例跑完之后,你会发现其中一个叫做ShowToolWindowNegativeTest的测试失败了,是我们的package有什么错误吗?(译者注:在VS 2008 SP1和VS 2008 SDK 1.1环境上并没有看到这个bug,所有的测试都通过了。)
这个测试用例用于模拟工具窗不能被创建时的情况。这个测试必须抛出一个异常,通过标记在它上面的attribute来表明它期望这个异常的出现:
[TestMethod()]
[ExpectedException(typeof(TargetInvocationException),
"Did not throw expected exption when windowframe object was null")]
public void ShowToolwindowNegativeTest() {...}
不幸的是,这个测试看起来并没有加ExpectedException这个attribute。
这其实是VSPackage向导的一个bug:它在SimpleTesting_UnitTestProject项目里添加了错误的引用。看一下Microsoft.VisualStudio.QualityTools.UnitTestFramework引用,你会发现它的版本是8.0.0,但这个版本是给VS 2005用的。知道这一点,我们改起来也简单了:移除这个引用,并引用正确的版本。另外,SimpleTesting_IntegrationTestProject项目并没有这个bug。(译者注:在本人的VS 2008 SP1和VS 2008 SDK 1.1环境上并没有看到这个bug,引用的版本是正确的。)
修复了这个bug之后,所有的测试用例都会运行通过了。
补上一个漏掉的TestMethod
在SimpleTesting_UnitTestsProject项目的MyToolWindow.cs文件里,有一个WindowPropertyTest的测试用例,但它上面漏掉了TestMethod这个attribute。漏掉了这个attribute并不会带来什么错误,这不过这个测试用例不会运行而已。如果想让它运行,我们可以手动的给它添加一个TestMethod:
[TestMethod()]
public void WindowPropertyTest() {...}
然后再一次运行所有的测试,WindowPropertyTest应该会运行通过。
CodePlex和Team Explorer的整合问题
还有另外一个问题耗费了我很长时间。我把所有的代码放到了CodePlex上(它用的是Team Foundation Server)。当我链接上TFS之后,如果运行解决方案下所有的测试的话,我的Visual Studio会死掉,好几分钟都没有响应。在5-9分钟之后,这些测试才开始运行。如果我断开链接的话,所有的东西都很正常,并且在我重新连上的话,这些测试也都正常了。但是如果重启一下Visual Studio并且连上TFS,它又会死掉。
如果你没有遇到这个问题的话,你可以忽略掉这部分,因为我也不想烦你…
在CodePlex上,我有好多个单元测试项目,但都没有这个问题,只有在测试项目和VSPackage有关的时候才会出现这个问题。我发现当VS死掉的时候,我的无线网络适配器那里产生了很大的流量。通过分析网络监听,我发现在这段时间,我的网络适配器从CodePlex网站上下载了差不多有38M的数据。由于我并不是一个网络专家,所以我就没有做进一步的研究。我现在的解决办法是在我要进行单元测试的时候就断开链接。
我会和VSX团队交流一下这个问题,希望他们能帮到我。也希望你们能够给我一些这个问题的建议。
把VS IDE作为测试的宿主
当运行测试的时候,测试用例需要一定的上下文信息,这样它们才能正确的执行。例如,如果测试用例用于转换一个文件,我们需要源文件和输出文件的上下文;如果我们需要测试一个带有数据访问层的业务逻辑,我们需要创建一个可以和数据访问层交互的上下文(或者一些看起来像数据访问层的组件)。
Visual Studio在一个单独的进程中运行单元测试,测试用例会认为它们运行在真实的环境中。例如当我们运行简单的单元测试时,它们通常运行在VSTestHost.exe进程里。为什么会运行在独立的进程中呢?这是有很多原因的,其中一个最重要的原因就是把测试和VS IDE进程分隔开。如果由于某种严重的问题导致测试进程挂掉的话(例如无线递归导致的堆栈溢出),VS IDE不会受到影响,甚至VS IDE可以捕捉并报告这个问题。
那么,当我们测试VSPackage的时候,上下文是什么呢?这就要看我们要测试的是什么了。如果我们测试的仅仅是算法、Helper类、简单的服务,这其实是“传统”的测试用例,VSTestHost.exe、NUnit或CSUnit都可以作为很好的上下文。
但是,如果我们想测试package的界面或和VS的集成情况——例如测试我们的package是否会创建菜单项——,我们需要另外一个可以模拟VS IDE的上下文。有一个众所周知的测试模式,叫做mocking,可以模拟我们的测试用例需要的上下文。大部分测试工具,包括NUnit和Visual Studio,都很好的支持mocking。
如果我们的VSPackage的测试用例能够运行在VS IDE(devenv.exe)里该有多好?它是用来测试package的最好的地方了。实际上,VSPackage向导生成的测试用例确实运行在devenv.exe里!当运行这些测试的时候,实际上启动了VS 2008实验室实例,并加载了测试用例需要的程序集。
选择VS IDE作为测试的宿主
Visual Studio里的调试——顺便说一句,也可以通过VSX来扩展调试——,使得通过所谓的测试适配器来把任何一个进程作为测试的宿主成为了可能。这篇文章(甚至整个系列)不会深入的讲解这些,但如果你兴趣的话,可以查看一下VS 2008 SDK里的VSIDEHostAdapter示例。
有很多种方式可以让Visual Studio用devenv.exe(用实验室模式)来运行我们的测试:
VSPackage向导在生成测试项目的时候,它同时生成了两个扩展名是testrunconfig的解决方案项。打开其中的任何一个(译者注:实际上只有打开IntegrationTests.testrunconfig才会看到下面的效果,因为另外一个测试项目SimpleTesting_UnitTestProject并不需要把VS IDE作为宿主,它用的是mocking),在主机(Hosts)标签下,可以看到主机类型(Host Type)设成了VS IDE:
VS IDE测试适配器可以设置上下文的参数:VS启动时使用的注册表项。另外,如果把主机类型(Host Type)设成了默认值(Default),测试用例就会在默认的主机类型中运行。
你可以用HostType这个attribute来为每一个单元测试设置主机类型,如下:
[TestMethod()]
[HostType("VS IDE")]
public void LaunchCommand() { ... }
上述代码段摘自SimpleTesting_IntegrationTestProject项目的MenuItemTest.cs文件,注意这个attribute的值设成了“VS IDE”。如果你在.testrunconfig文件里把主机类型设置成默认值,并且把这个测试用例的HostType设成了“VS IDE”,这个用例会在VS IDE中运行,因为HostType这个attribute会覆盖掉testrunconfig中的设置。
我们可以做一些实验。把两个testrunconfig文件的主机类型都设成默认,然后运行所有的单元测试,你会发现有两个测试失败了:CPPWinformsApplication和VBWinformsApplication。这是怎么回事呢?
其他的测试都运行成功了,因为它们要么不需要VS IDE,要么有[HostType(“VS IDE”)]。试一下给这两个失败的测试方法添加上这个attribute,然后再一次运行所有的测试,你会看到所有的测试又可以通过了。好了,别忘了把testrunconfig文件再还原回去:主机类型设成VS IDE,Visual Studio Registry Hive设成9.0(RANU)。
如果你有需要以VS IDE作为宿主的单元测试,我建议你按照下面的最佳实践来做:
- 把需要VS IDE的测试和不需要VS IDE的测试分开(可以通过程序集、命名空间、文件夹、文件或者其他任何你觉得比较好的方法)
- 把需要VS IDE的程序集对应的主机类型设成VS IDE
- 为相应的测试方法显式的添加[HostType(“VS IDE”)]
深入研究一下这些测试项目
如果非要用一个文雅的词汇来描述VS 2008 SDK中关于测试的文档的话,我会用“少的可怜”这个词。我认为VSX团队在这方面要做的事情还很多。深入研究一下VSPackage向导生成的测试项目是很值得的,因为我们可以在这些代码中得到很多文档里没有提到的信息。VSPackage向导生成了两个测试项目,我们来逐一看一下它们。
UnitTestProject
SimpleTesting_UnitTestProject利用mocking来在模拟的上下文中运行测试用例。任何一个VS Shell的服务和这些服务的所有方法的调用都通过mocking来模拟,所以我们的package会“感觉”它自己运行在VS IDE中。这个项目包括如下几个测试用例:
测试用例 | 描述 |
CreateInstance |
检查创建package的时候(调用默认构造函数)不会抛出异常。 |
IsIVsPackage |
检查package是否实现了IVsPackage接口。 |
SetSite |
检查package是否可以正确的被site和unsite。 |
InitializeMenuCommand |
检查我们的package里的菜单命令是否可以添加到VS的菜单中。 |
MenuItemCallback |
检查我们的位于工具菜单下的菜单的回调方法是否能正常工作(如果在产品环境下的话,这个菜单会弹出一个简单的消息框)。 |
MyToolWindowConstructorTest |
检查我们的工具窗是否可以被创建,并且对应的用户控件也能够被创建。 |
WindowPropertyTest |
检查是否可以通过工具窗类的Window属性来获取工具窗界面的引用。 |
ValidateToolWindowShown |
检查ShowToolWindow方法是否可以显示工具窗。 |
ShowToolwindowNegativeTest |
检查不能被VS创建的工具窗是否不能显示 |
这些单元测试都非常简单,并且只负责测试很简单的功能。如果你想搞清楚在一个单元测试运行的背后到底发生了什么,你最好先搞清楚VSPackage的mocking是怎样工作的。虽然这已经超出了本文的范围,但我会给你一些对你有用的指引。
这些测试项目有一个对Microsoft.VSSDK.UnitTestLibrary的引用,该程序集包含一些单元测试和mocking的helper类。在VS 2008 SDK 1.0里,你可以看到这个程序集的源代码,它们位于VS SDK的VisualStudioIntegration\Common\Source\CSharp\UnitTest目录下。通过分析这些源码,我感觉这个程序集仅仅是一个初级的东西,因为它只包含了很少一些类型。我不知道将来这个程序集会包含什么功能,但我可以想象,在将来,这个程序集会改变成另外一个样子。不过目前这些源码可以作为理解如何实现mocking的基础。
SimpleTesting_UnitTestProject包含3个带有Mock后缀的文件,它们为我们的单元测试模拟了一个非常简单的上下文。
为了帮助你们理解这些单元测试是如何工作的,让我来解释一下其中的一个: ValidateToolWindowShown。这个测试方法用于检查我们的package的ShowToolWindow私有方法是否能够正确运行。让我们看一下ShowToolWindow方法的实现:
private void ShowToolWindow(object sender, EventArgs e)
{
ToolWindowPane window = this.FindToolWindow(typeof(MyToolWindow), 0, true);
if ((null == window) || (null == window.Frame))
{
throw new NotSupportedException(Resources.CanNotCreateWindow);
}
IVsWindowFrame windowFrame = (IVsWindowFrame)window.Frame;
Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(windowFrame.Show());
}
如果你忘了它是怎样工作的,请看一下第4篇文章。FindToolWindow方法通过SVsUIShell服务的CreateToolWindow方法来创建我们的工具窗。好,让我们来看一下在测试用例中的代码:
1: [TestMethod()]
2: public void ValidateToolWindowShown()
3: {
4: IVsPackage package = new SimpleTestingPackage() as IVsPackage;
5:
6: // Create a basic service provider
7: OleServiceProvider serviceProvider = OleServiceProvider.CreateOleServiceProviderWithBasicServices();
8:
9: //Add uishell service that knows how to create a toolwindow
10: BaseMock uiShellService = UIShellServiceMock.GetUiShellInstanceCreateToolWin();
11: serviceProvider.AddService(typeof(SVsUIShell), uiShellService, false);
12:
13: // Site the package
14: Assert.AreEqual(0, package.SetSite(serviceProvider), "SetSite did not return S_OK");
15:
16: MethodInfo method = typeof(SimpleTestingPackage).GetMethod("ShowToolWindow", BindingFlags.NonPublic | BindingFlags.Instance);
17:
18: object result = method.Invoke(package, new object[] { null, null });
19: }
这个测试用例用到了两个mock实例,OleServiceProvider类来自于Microsoft.VsSDK.UnitTestLibrary命名空间,它是Microsoft.VisualStudio.Shell命名空间下的OleServiceProvider的mock,第7行创建了这个mock实例。在第10行里,UIShellServiceMock类型创建了SVsUIShell服务的mock对象。第11行把SVsUIShell的mock对象加到了可用的服务中。当ShowToolWindow方法调用FindToolWindow时,就轮到我们的SVsUIShell的mock对象表演了:
private void ShowToolWindow(object sender, EventArgs e)
{
ToolWindowPane window = this.FindToolWindow(typeof(MyToolWindow), 0, true);
...
}
从第14行开始真正的测试代码,在这一行里我们的package被site到已经mock的上下文中。第16-18行调用package的ShowToolWindow私有方法,如果这个调用没有抛出异常的话,我们的测试就算通过了。
为了更进一步了解mock的实现,让我们看一下这个项目里的UIShellServiceMock类定义。在这个项目里,有两个UIShellServiceMock类,我们看一下MyToolWindowTest文件夹里的那个。下面的代码段并不是全部的源代码,但包含了最重要的部分:
using System;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VsSDK.UnitTestLibrary;
namespace UnitTestProject.MyToolWindowTest
{
static class UIShellServiceMock
{
private static GenericMockFactory uiShellFactory;
internal static BaseMock GetUiShellInstance()
{
if (uiShellFactory == null)
{
uiShellFactory = new GenericMockFactory("UiShell",
new Type[] { typeof(IVsUIShell), typeof(IVsUIShellOpenDocument) });
}
BaseMock uiShell = uiShellFactory.GetInstance();
return uiShell;
}
internal static BaseMock GetUiShellInstanceCreateToolWin()
{
BaseMock uiShell = GetUiShellInstance();
string name = string.Format("{0}.{1}", typeof(IVsUIShell).FullName,
"CreateToolWindow");
uiShell.AddMethodCallback(name,
new EventHandler<CallbackArgs>(CreateToolWindowCallBack));
return uiShell;
}
private static void CreateToolWindowCallBack(object caller,
CallbackArgs arguments)
{
arguments.ReturnValue = VSConstants.S_OK;
IVsWindowFrame frame = WindowFrameMock.GetBaseFrame();
arguments.SetParameter(9, frame);
}
...
}
}
静态字段uiShellFactory是GenericMockFactory类型的,它可以创建mock对象,是通过在运行期创建动态程序集和动态类型做到的。GetUiShellInstance方法用于获得mock实例。
测试用例通过调用GetUiShellInstanceCreateToolWin方法来获得mock实例,在这个方法内部,它用AddMethodCallback来注册一个mock方法(在这里是CreateToolWindow)。当通过这个mock对象调用CreateToolWindow方法时,它实际上调用了CreateToolWindowCallBack 方法,该方法创建了一个工具窗,并且设置了参数和返回值。
我认为很值得再看看其他的测试用例,你会从中发现很多VS shell和它的服务是如何工作的信息。
IntegrationTestProject
SimpleTesting_IntegrationTestProject项目用于VS集成测试,测试用例会运行在VS 2008实验室里。如果查看一下这个项目的源代码的话,除了可以找到这些测试用例,还会发现一些很有用的文件。我建议你们看一下这些文件,里面肯定有你们感兴趣的内容:
文件名 | 描述 |
NativeMethods.cs |
用user32.dll和kernel32.dll声明一些Win32 API |
Utils.cs |
定义了集成测试用到的很多helper方法,这个文件对我们这些vsx的初学者很有帮助:我们可以在这里找到很多“how-to”的答案。在这里我仅仅提一下其中几个方法的名字(当然还有很多其他的方法):CreateEmptySolution, ForceSaveSolution, AddNewItemFromVsTemplate, SaveDocument! |
DialogBoxPurger.cs |
这个文件可以用来关闭由VS调用弹出来的消息框。虽然这个文件有几百行代码,但它的用法是很简单的、很让人“惊叹”的。 |
IntegrationTestProject项目包含如下的测试用例:
测试用例 | 描述 |
PackageLoadTest |
检查package是否可以被VS IDE加载 |
LaunchCommand |
检查我们的package里位于工具菜单下的菜单项是否能正常工作:显示消息框并关掉它。这个测试用例可以看作是使用DialogBoxPurger类的一个例子。 |
ShowToolWindow |
检查用于显示工具窗的菜单项是否可以正常工作。 |
CPPWinformsApplication |
这几个测试用例用于检查:当我们的package已经被加载到VS IDE之后,C++、C#和VB的winform application是否还能够被正常创建。也就是说,这几个测试用例用于检测我们的package是不是有副作用。 |
CreateEmptySolution |
检查当我们的package加载到VS IDE之后,是否还能够创建一个空的解决方案项目。和上面一样,这个测试的目的也是为了检查我们的package是否有副作用。 |
我建议你先从CreateEmptySolution这个测试方法看起,然后是那几个Winforms的测试代码。每个测试的方法体都被包在UIThreadInvoker里:
[TestMethod][HostType("VS IDE")]
public void CreateEmptySolution()
{
UIThreadInvoker.Invoke((ThreadInvoker)delegate()
{
...
});
}
UIThreadInvoker方法会在当前UI线程里执行Invoke方法的参数所指定的委托代码。这些委托代码用来模拟用户操作。
我建议你们不要光看这些代码,还得调试它们才行。
总结
在这篇文章里,我们为package创建了测试用例,并深入的看了这些VSPackage向导生成的测试方法。向导生成的代码有一些“问题”,不过我们修复了它们。
向导帮我们生成了两个测试项目:
- _UnitTestProject:为我们的package做一些基本的“健康测试”,它们用mocking模式来模拟,使得package看起来像是加载到了VS中。
- _IntegrationTestProject:测试我们的package是否能加载到VS IDE中,并且不会带来任何副作用。这些测试运行在VS 2008实验室模式里。
目前的VS 2008 SDK中只包含了很少一部分关于package测试的文档,Microsoft似乎想为package的测试而开发工具集,但还没有完全开发好。所以在这之前,我们可以利用VSPackage向导生成的代码为基础和参考,来写自己的测试代码。
出处:http://www.cnblogs.com/default
本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接,否则保留追究法律责任的权利。