(翻譯)LearnVSXNow! #11- 測試package
- 2019 年 10 月 5 日
- 筆記
在前面的文章里,當我們創建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的VisualStudioIntegrationCommonSourceCSharpUnitTest目錄下。通過分析這些源碼,我感覺這個程式集僅僅是一個初級的東西,因為它只包含了很少一些類型。我不知道將來這個程式集會包含什麼功能,但我可以想像,在將來,這個程式集會改變成另外一個樣子。不過目前這些源碼可以作為理解如何實現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 | 檢查用於顯示工具窗的菜單項是否可以正常工作。 |
CPPWinformsApplicationWinformsApplicationVBWinformsApplication | 這幾個測試用例用於檢查:當我們的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嚮導生成的程式碼為基礎和參考,來寫自己的測試程式碼。