尤曉洺 蔡先華
(東南大學交通學院地理信息工程系 江蘇 南京 210096)
單元測試方法在GIS開發中的應用研究
尤曉洺 蔡先華
(東南大學交通學院地理信息工程系 江蘇 南京 210096)
單元測試是在軟件測試中要進行的最低級別的測試活動,是保證軟件質量的第一環。然而由于GIS開發的一些復雜特征,很多GIS開發者放棄了編寫單元測試。本文針對GIS開發的特點,探討了GIS開發中的編寫單元測試的一些問題。包括如何在單元測試中加載第三方GIS組件許可,如何改善設計使單元測試易于編寫,如何在單元測試中和復雜組件對象交互等。通過對這些方法的應用,解決GIS開發中編寫單元測試的一些問題。
單元測試;GIS開發;NUnit
隨著社會對GIS的需求越來越多,GIS軟件規模的不斷擴大,GIS軟件設計的復雜程度不斷提高,軟件開發中出現錯誤或缺陷的幾率也越來越大,如何保證GIS軟件的質量,使GIS軟件為社會提供穩定而且正確的服務,是一個值得研究的問題。軟件測試是驗證軟件質量的有效手段,而單元測試是保證軟件質量的第一環。然而據2008年的一份統計,超過48%的GIS開發者不編寫任何單元測試,這所導致的是他們的項目有超過50%成本都消耗在了系統維護上[1]。本文就如何在GIS開發中編寫單元測試進行討論。
單元測試是開發者編寫的一段可以自動執行的代碼,用于證明被測試代碼的行為和開發者所期望的一致[2]。所以單元測試應該是由程序員自己來完成的,它往往是對一個個基本的功能單元進行的測試。
舉一個簡單的例子,現在有一個編寫好的函數,函數的接受一個整型數組作為參數,返回這個數組中值最大的數??梢跃帉戇@樣一個單元測試用例:把數組{1,2,3,4}作為參數傳遞給這個函數,判斷這個函數的返回值的是否等于4,如果不是,就說明這個函數的代碼是錯誤的??梢允褂脝卧獪y試框架,如NUnit[3],編寫單元測試,斷言(Assert)這個函數在接受上述參數時的返回值為4,單元測試框架可以使這個測試和其它的單元測試自動執行,并報告哪些測試用例沒有通過。
好的單元測試測試應該是自動的、全面的、可重復的、獨立的和專業的[2]。正確的使用單元測試可以起到如下作用:驗證代碼行為;通過編寫單元測試,解除代碼中的耦合;單元測試可以作為軟件開發的文檔,它是可編譯、可運行的,與代碼同步;自動化的單元測試避免了代碼出現回歸。
本文以使用ArcGIS Engine for.Net進行GIS開發為例,使用為. Net開發而設計的自動測試框架NUnit,探討一下GIS開發中單元測試編寫的一些問題。如果使用Visual Studio進行開發,結合使用免費的TestDriven.Net Personal Version,可以非常方便快捷地在開發環境中執行和調試單元測試,幫助提高開發效率[5]。
在使用ArcGIS Engine中的對象(可以看作是ArcObjects的子集)時是需要加載許可的。同樣,在使用到ArcObjects的單元測試,也是需要加載許可的。單元測試不同于普通的應用程序,沒有顯式的程序入口,沒有明確的執行順序,它是被Test Runner加載并執行的,那如何在單元測試中加載許可是GIS開發中編寫單元測試所要解決的一個問題。如果只有少數幾個Test Fixture使用到了ArcObjects,可以在這些Test Fixture中標記了TestFixtureSetUp屬性的方法中加載許可,在標記了TestFixtureTearDown中卸載ArcObjects,如下:

如果有很多Text Fixture中都使用到了ArcObjects,這樣做會降低測試執行的效率,可以采用另外一種更通用的方法,在標注了SetUpFixture屬性的類中的SetUp方法加載許可,TearDown方法中卸載ArcObjects,如下:

在上述代碼中,標記了SetUp屬性的LoadLicense方法會在執行這個類AeLicenseLoader所在的命名空間中所有測試前執行,而標記了TearDown屬性的UnloadAoObjects方法會在執行完這個類所在的命名空間中所有的測試后執行。如果要為整個程序集(Assamble)提供運行許可,把類AeLicenseLoader置于全局命名空間即可,這樣在執行這個程序集任何測試前,會加載許可,執行完測試后,卸載ArcObjects。
有時候會發現很難對一些功能代碼編寫單元測試,這往往暗示著設計需要修改,不應該放棄編寫單元測試,需要做的是修改設計,分離代碼的關注點,直到代碼易于測試。易于測試的代碼往往有更好的結構和可維護性[2]。
例如有些程序員習慣將業務邏輯的代碼混雜在用戶界面的代碼中,這本身并不是一種優秀的設計,也使單元測試編寫起來變得非常困難。應該將業務邏輯的代碼和用戶界面的代碼進行分離,使用諸如三層架構(表示層、業務邏輯層和數據訪問層三層架構)或MVC(模型、視圖和控制器)模式等方法對代碼進行重構。這樣既改善了設計,也使單元測試的編寫變得容易。類似的,要避免編寫出職能過多的類,這樣的類不僅很難測試,也很難維護,是違背面向對象設計的原則的,應用一些模式,改善設計,使之易于測試。也可以通過實踐驅動測試開發,改善代碼的設計。
然而ArcObjects是一個龐大而且復雜的系統,ArcObjects中的很多對象甚至是很難構建的,如果在功能代碼中使用到這些對象,真的需要為了編寫單元測試而構建這些對象嗎?
首先需要注意的是單元測試所測試的目標。編寫的單元測試是要測試自己編寫的功能代碼,而不是ESRI的ArcObjects。比如對一個面的緩沖區操作,對兩個幾何對象求交,或者是修改了Geodatabase中的一個要素的屬性值,這些類似的代碼只是對ArcObjects的簡單調用,如果對這些代碼的行為不確定,需要做的是參考ArcObjects的幫助文檔,當然如果對這些代碼的行為還是不確定,也可以編寫單元測試進行驗證,但是這樣做的代價往往會很高。
如果確定了ArcObjects對象的行為,要測試和ArcObjects中復雜對象進行交互的功能代碼時,我們可以使用Mock對象的技術[6],對功能模塊進行測試。這種測試方法的思想是用模擬的對象替代真實的對象,模擬出一個測試環境,對功能代碼進行測試,Mock對象就是真實對象在調試期的替代品。而且由于ArcObjects是基于接口的架構,正適合使用這種技術。可以自己編寫Mock對象,也可以使用現有的Mock對象框架,如NMock2、DotNetMock和RhinoMocks。
ArcObjects中一個經常到對象的類型是Feature類,而它的對象又是很難用代碼從頭構建的。下文中的CountyFeature類就使用到了表示一個縣域要素的對象,它對一個縣域要素進行了強類型化的封裝。部分代碼如下:

CountyFeature類的構造方法接受一個表示縣域要素的參數,縣域要素有一個字段的值是該縣域的政區代碼,這個代碼是符合中華人民共和國行政區劃代碼標準(GB2260-1995)的。根據這個縣域的政區代碼可以判斷這個要素是否是省直轄縣級市,CountyFeature類的一個屬性就實現了這個功能,它返回該縣域是否是省直轄縣級市?,F在編寫單元測試對這個功能代碼進行測試。那么就需要實例化CountyFeature類的對象,而CountyFeature類的構造方法需要一個IFeature類型的縣域要素作為參數。編寫測試時需要這個參數,獲得這個參數可以使用真實的數據,也就是說,使用包含縣域要素類的Geodatabase,假設這個數據是容易獲取的,但是仍然需要在單元測試代碼中編寫許多根本不是這個測試所需要關注的代碼,而且由于這些代碼訪問外部資源,測試運行的速度也會變慢,所以不應該使用這種方法。
使用Mock對象就可以解決上述這個問題。判斷這個縣域是否是省直轄縣級市的功能代碼關注的只是縣域要素區域政區代碼字段的值,所以可以編寫一個帶有政區代碼的仿真縣域要素的對象,其他至于縣域要素到底有沒有其它字段等都不是這個測試所關心的內容。IFeature接口有9個屬性,4個方法,如果自己編寫這個Mock對象的話同樣會浪費很多時間,解決方法就是使用動態Mock對象,動態Mock對象會在運行時構建一個指定類型的 Mock對象。本文以NMock2中的動態Mock對象為例,編寫的對上述功能的測試代碼如下:

測試方法的第三行代碼表示,使當以指定參數調用 feature.get_Value方法時,返回regionCode的值。上述測試代碼隔離了和測試目標無關的內容,使測試專注于需要測試的功能。
Mock對象是一項很實用的技術,但由于需要編寫更多的代碼,特別是自己編寫Mock對象,無疑會加重項目的負擔,有時我們可以通過一些重構消除對Mock對象的依賴。
盡管使用了如上所述的方法,但在GIS開發中還是不可避免地需要使用到一些復雜的對象。典型的例子是ArcObjects中的幾何數據類型。例如需要自己實現兩個多邊形求交的功能,那么如何對這個功能進行測試。在測試代碼中需要構建兩個多邊形對象,還有這兩個多邊形求交后的正確結果,然后將真實的求交結果和正確望的結果進行比較,如果不相同,就說明我自己實現的這個功能是錯誤的。按照單元測試應該是全面的原則,需要編寫多個測試用例。對于那些簡單的多邊形對象,可以編寫代碼進行構建(新建一個多邊形對象,定義它的空間參考和所有節點)。如果是復雜的多邊形對象,這種方法會給編寫單元測試帶來較大的負擔,我們可以從一個文件中(如Shapefile)讀取一個多邊形,但是這樣做會牽扯到更多的與測試不相關的對象(如工作空間,要素類等對象),降低了單元測試編寫和執行的效率。ArcObjects中有個IXMLSerialize接口,ArcObjects中有超過340個類實現了這個接口,其中也包括幾何數據類,實現了這個接口的類,可以將這些類的對象序列化為xml,也可以從xml中構建這些對象??梢允褂眠@個功能在編寫單元測試時從xml中構建一些復雜的對象。一個名為ArcUnit的開源項目,實現了一個ArcMap的工具擴展,它可以將選中要素的Shape保存成xml文件,方便在編寫單元測試時構建復雜的幾何對象[7]。
編寫單元測試本身是需要時間的,但是從長遠角度來看,單元測試對提高團隊開發的效率和軟件質量都有較大的作用,同時還能改善設計,提升bug修復的效率。因此,很有必要在GIS開發中編寫單元測試。在發現很難對一些功能代碼編寫單元測試時,優先考慮改善設計,再考慮使用Mock對象等其他技術方法,編寫單元測試。解決了GIS開發中編寫單元測試這些細節問題,實踐測試驅動開發,以單元測試作為開發過程的開端并且將測試引入到系統開發的全過程,保證了代碼的階段正確性,避免了沒有單元測試的GIS開發中后期集中測試產生問題時的舉步維艱,同時單元測試代碼這份可以運行的文檔可以指導其他開發者如何使用相應的功能代碼。
[1]Dave Bouwman.Developer Survey[EB/OL].http://blog.davebouwman.com/2008/ 06/04/developer-survey-unit-testing-other-tools/,2008.
[2]Andy Hunt,Dave Thomas,Matt Hargett.Pragmatic Unit Testing in C#with Nunit,Second Edition[M].The Pragmatic Bookshelf,2007.
[3]NUnit.org.NUnit Documentation[EB/OL].http://www.nuint.org/.
[4]NCover LLC[EB/OL].http://www.ncover.com/.
[5]TestDriven.Net.Quickstart[EB/OL].http://www.testdriven.net/quickstart.aspx.
[6]Roy Osherove.The Art of Unit Testing with Examples in.NET[M].Greenwich:Manning,2009.
[7]Brian Noyle,David Bouwman.Unit Testing for Esri Developers[J].ArcUser,2010,Winter:38-41.
王靜]