在像 Eclipse 那樣的 IDE 中或者比如在 Ant 構建腳本中運行單元測試是確保應用程序質量的一個很好的開始;然而,版本控制庫(如 Subversion)中的源代碼一改變,在單獨無變動的構建機上運行單元測試就有助于檢驗開發生命周期中的問題。而且,運行各種類型的開發人員測試,如組件測試、功能測試和性能測試,能夠在開發生命周期中更早地 將問題顯示出來。
通常在持續集成(CI)環境中運行的開發人員測試有效地扮演著代碼質量聚光燈的角色。這是因為如果能有效地編寫這些測試,則幾乎能夠在問題(如缺陷)產生之時就將其發現。不經常運行測試通常就不怎么有效,因為從產生缺陷到發現該缺陷的時間相隔很長,但持續地(即,每一次代碼改變時)運行測試能確?焖侔l現無意識的行為。
本文涵蓋下列內容:
通過 Ant 運行 JUnit 測試
使用 JUnit 和 DbUnit 執行更長時間的運行組件測試
使用 JUnitPerf 確定哪些方法花費時間過久而執行失敗
用 Selenium 運行基于 Web 的功能測試
用 Cobertura 訪問代碼覆蓋率
用 CruiseControl 進行持續測試
我提供一個關于不同類型開發人員測試的概覽,和一些可以添加到構建過程并使用 Continuous Integration 系統持續運行的例子。
按 JUnit 進行單元測試
有時,我聽到開發人員將開發人員測試這一術語與簡單的單元測試相混淆;然而,我發現將單元測試這一術語提練得更加明確很有幫助。對我來說,單元測試是快速運行的 測試,通常測試沒有大的外部依賴項(如數據庫)的單獨的類。例如,清單 1 定義了一個單元測試,該測試使用 JUnit 來驗證一個叫做 BeerDaoStub 的存根數據類。針對并未真正連接到數據庫的接口的測試技術是一種驗證業務功能的方法,使用該方法不會導致花費昂貴的設置成本。另外,這樣做可使測試保持為一個真正的單元測試。
清單 1. 一個簡單的單元測試
public void setUp() {
beerService = new BeerDaoStub();
}
public void testUnitGetBeer() {
Collection beers = beerService.findAll();
assertTrue(beers != null && beers.size() > 0);
}
一旦編寫了一些單元測試,就可以一直通過 IDE 運行這些測試,但您也想要將這些測試作為構建過程的一部分來運行。確保該測試通過構建過程成功運行意味著也能從 CI 構建的上下文中啟動這些相同的測試。
清單 2 是一個 Ant 腳本片段,介紹了執行一批單元測試的 junit 任務。這項任務與 JUnit 一起運作,其妙處在于:定義過的所有測試現在都能自動運行并且如果其中任何一個測試失敗,則構建也將失敗 —— 通過使用 haltonfailure 屬性實現。
清單 2. 在 Ant 中運行單元測試
<junit fork="yes" haltonfailure="true" dir="${basedir}" printsummary="yes">
<classpath refid="test.class.path" />
<classpath refid="project.class.path"/>
<formatter type="plain" usefile="true" />
<formatter type="xml" usefile="true" />
<batchtest fork="yes" todir="${logs.junit.dir}">
<fileset dir="${test.unit.dir}">
<patternset refid="test.sources.pattern"/>
</fileset>
</batchtest>
</junit>
注意:test.unit.dir 指定測試的位置。這是將這些測試(在本例中為單元測試)和其他測試隔離起來的有效方法。通過利用這項技術,可以通過定義另外的 Ant 目標來先運行較快的測試,接著運行較慢的測試(如組件測試、功能測試和系統測試)。
集合組件測試
由于單元測試執行得相當快,很容易將它們作為構建的一部分經常運行。但這些測試并未達到一個高的代碼覆蓋率 —— 其隔離的本質決定了它們只測試一部分功能。編寫具有更多代碼(從而可實現更多功能)的測試通常要以附屬框架的形式執行更多的調查工作。一旦開始使用這些幫助框架來編寫測試,這些測試就開始成為更高級別的測試,我把它們歸類為組件測試。
組件測試是基本的測試,這些測試將驗證不止一個類,且通常依賴于外部依賴項,如數據庫。組件測試的編寫方式和單元測試大體一致,只是前者并非通過模擬或存根類來強制隔離,實現這些測試可謂勉為其難,但可以利用框架來便利對外部依賴項的使用。例如,我通常使用 DbUnit 框架來幫助管理數據庫,以便組件測試可驗證依賴數據庫數據的代碼功能。
用 DbUnit 控制數據庫狀態
DbUnit 是一個框架,它使針對數據庫的測試過程變得更加簡單。它提供了一個標準 XML 格式,用于定義一些測試數據,以便從數據庫中選擇、更新、插入和刪除數據。請牢記,DbUnit 并沒有替換數據庫;它只是提供了一種更加有效的機制來處理測試數據。您可以用 DbUnit 來編寫依賴于特定數據的測試,DbUnit 保證該數據位于底層的數據庫中。
可以在 JUnit 中可編程地使用 DbUnit,或者可以將它作為構建過程的一部分使用。該框架帶有一個 Ant 任務,該任務提供了一種使用 XML 文件來操作、導出或比較數據庫中數據的方法。例如,清單 3 演示了 dbunit 任務,在本文的例子中,該任務將測試數據插入到目標數據庫中,然后在運行完所有組件測試后刪除數據:
清單 3. 在 Ant 中運行組件測試
<target name="component-tests">
<mkdir dir="${logs.junit.dir}" />
<taskdef name="dbunit"
classname="org.dbunit.ant.DbUnitTask"/>
<dbunit driver="com.mysql.jdbc.Driver"
url="jdbc:mysql://localhost:3306/brewery"
userid="${db.username.system}"
classpathref="db.lib.path"
password="${db.password.system}">
<operation type="INSERT"
src="seedFile.xml"/>
</dbunit>
<junit fork="yes" haltonfailure="false"
failureproperty="tests.failed"
haltonerror="true" dir="${basedir}"
printsummary="yes">
<classpath refid="test.class.path" />
<classpath refid="project.class.path"/>
<formatter type="plain" usefile="true" />
<formatter type="xml" usefile="true" />
<batchtest fork="yes" todir="${logs.junit.dir}">
<fileset dir="${test.component.dir}">
<patternset refid="test.sources.pattern"/>
</fileset>
</batchtest>
</junit>
<mkdir dir="${reports.junit.dir}" />
<junitreport todir="${reports.junit.dir}">
<fileset dir="${logs.junit.dir}">
<include name="TEST-*.xml" />
<include name="TEST-*.txt" />
</fileset>
<report format="frames" todir="${reports.junit.dir}" />
</junitreport>
<dbunit driver="com.mysql.jdbc.Driver"
url="jdbc:mysql://localhost:3306/brewery"
classpathref="db.lib.path"
userid="${db.username.system}"
password="${db.password.system}">
<operation type="DELETE"
src="seedFile.xml"/>
</dbunit>
</target>
正如清單 3 所示,現在組件測試可在執行期間依賴駐留在數據庫中的特定數據。另外,由于在所有測試成功執行后刪除了所有的數據,因而此過程現在可重復執行。
在數據庫中播種
可以將 dbunit 任務的 INSERT 和 DELETE 操作類型和一個種子文件起使用,該文件包含表示數據庫表和相關行的 XML 元素。例如,清單 4 是清單 3 中引用的 seedFile.xml 文件的內容。每個 BEER 元素表示一個也叫 BEER 的數據庫表,BEER 元素的每個屬性和其值都映射至相應的數據庫列名稱和值。
清單 4. DbUnit 種子文件
<?xml version='1.0' encoding='UTF-8'?>
<dataset>
<BEER id='6'
beer_name='Guinness Extra Stout'
brewer='St.James Brewery'
date_received='2007-02-01' />
<BEER id='7'
beer_name='Smuttynose Robust Porter'
brewer='Smuttynose Brewery'
date_received='2007-02-01' />
<BEER id='8'
beer_name='Wolavers pale ale'
brewer='Wolaver Brewery'
date_received='2007-02-01' />
</dataset>
您也許已經從清單 3 中注意到,可以在不同的操作中重用 DbUnit 的種子文件。在本文的例子中,將在運行組件測試前使用清單 4 中的文件在數據庫中播種,然后使用相同的文件指示測試完成時從數據庫中刪除哪些數據。
參與性能測試
開發人員完成編碼后,常常要經過很長時間才執行性能測試,而事實通常是可以在開發周期中更早的時候發現(并且解決)性能問題。幸運地是,有一種方法可解決此問題:持續測試或更具體地、持續地運行 JUnitPerf 測試。
對性能測試來說 JUnitPerf 是完美的
JUnitPerf 是一個同 JUnit 協調工作的框架,該框架在一個預定的時間限制內執行測試用例:如果一個測試中的方法所用的時間比預期的閾值長,則認為該測試是失敗的。通過將性能測試集成到自動化構建中,您能有效地監控應用程序的性能甚至能在出現性能問題時使構建失敗。
但我傾向于將 JUnitPerf 用作一種發現早期性能問題的簡單方法,而不是將其作為一種機制來衡量執行時間;像 profilers 這樣的工具更善于提供此類衡量。在本質上,可以認為 JUnitPerf 是一個早期的警告系統。
在清單 5 中,我定義了一個 JUnit 測試,該測試使用 JUnitPef 來驗證 BeerServicePerformanceTest 測試類中的 testLongRunningMethod 測試的執行時間。如果執行該測試方法所花的時間多于 1000 毫秒,則測試失敗。
清單 5. 使用 JUnitPerf 的基于性能的測試
package com.beer.business.service;
import com.clarkware.junitperf.*;
import junit.framework.Test;
public class ExampleTimedTest {
public static Test suite() {
long maxElapsedTime = 1000;
Test testCase = new BeerServicePerformanceTest("testLongRunningMethod");
Test timedTest = new TimedTest(testCase, maxElapsedTime);
return timedTest;
}
public static void main(String[] args) {
junit.textui.TestRunner.run(suite());
}
}
使用精確計時作為方法執行時間的標準時要小心;測試的建立和銷毀時間包含在整個執行時間中。此外,在早期的性能測試中,精確測定執行速度在更大程度上是一門藝術而不是科學。
使用 Selenium 進行功能測試
可隨意編寫所有需要的單元測試和組件測試,但如果要編寫一個提供某種類型的用戶界面的應用程序(例如 Web 應用程序),則需要測試表示層。以 Web 應用程序為例,需要驗證用戶場景的導航,另外還要驗證場景的功能是正常的。盡管如此,直到最近,這類測試都常被證明是一個負擔,需要購買工具來促進開發周期晚期的測試。此外,這些工具幾乎不能適合構建過程,即使測試構建得足夠早也是如此。
深入 Selenium
但近幾年來,一些著眼于功能測試的開放源碼工具脫穎而出;而且,能輕易地在開發生命周期的早期使用這些工具。工具如 Selenium 和 Watir 都是開放源碼的;另外,它們構建時考慮到了開發人員。除了用各種語言(例如 Java 編程和 Python)編程定義 Selenium 測試之外,Selenium 也提供了一種易于學習的表格驅動格式,此格式也能被非技術類型使用。
Selenium 框架使用 JavaScript 來執行基于 Web 的接受測試,該測試打開一個瀏覽器并運行表格驅動測試。例如,清單 6 展示了一個表示簡單的 Selenium 測試的 HTML 表。該測試的多個步驟打開一個 Web 應用程序,然后使用有效的用戶名和密碼執行登錄。測試結果生成到一個 HTML 表中,在 Selenium 運行完所有的測試后,能查看該表。
清單 6. 使用 Selenium 的功能測試
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>MyTest</title>
</head>
<body>
<table cellpadding="1" cellspacing="1" border="1">
<thead>
</thead><tbody>
<tr>
<td>open</td>
<td>/beer/</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>username</td>
<td>admin</td>
</tr>
<tr>
<td>type</td>
<td>password</td>
<td>password</td>
</tr>
<tr>
<td>clickAndWait</td>
<td>//input[@value='Login']</td>
<td></td>
</tr>
<tr>
<td>verifyTextPresent</td>
<td>Logged in as admin</td>
<td></td>
</tr>
</tbody></table>
</body>
</html>
使用清單 6 中基于表格的格式,可以定義多個接受測試。也可以將測試分組成套,一次執行一整套測試。
使用 Ant 驅動 Selenium
Selenium 的偉大之處在于它是在考慮了 CI 的基礎上從頭創建的,因為你能在像 Ant 那樣的構建工具中運行 Selenium。此外,由于框架設計者的高瞻遠矚,如果任何 Selenium 接受測試失敗,您也可以讓整個構建失敗。例如,清單 7 展示了一個 Ant 任務,該任務使用 Selenium 遠程控制服務器在一個 Web 應用程序中執行一系列表格驅動測試:
清單 7. 使用 Ant 運行 Selenium
<?xml version="1.0" encoding="iso-8859-1"?>
<project name="functional-tests" default="run-selenium-tests" basedir=".">
<property file="${basedir}/selenium.properties"/>
<import file="${basedir}/common-environment.xml"/>
<property name="acceptance.test.lib.dir"
value="${functional.test.dir}" />
<property name="firefox" value="*firefox" />
<property name="base.url"
value="http://${web.host.name}:${web.port}" />
<property name="acceptance.test.list.dir"
value="${functional.test.dir}" />
<property name="acceptance.test.report.dir"
value="${functional.test.dir}" />
<target name="run-selenium-tests">
<mkdir dir="${reports.dir}" />
<java jar="${acceptance.test.lib.dir}/selenium-server.jar"
fork="true">
<arg line="-htmlSuite "${firefox}""/>
<arg line=""${base.url}""/>
<arg line=""${acceptance.test.list.dir}/${test.suite}""/>
<arg line=""${reports.dir}/index.html""/>
<arg line="-timeout ${timeout}"/>
</java>
</target>
<target name="stop-server">
<get taskname="selenium-shutdown"
src="http://${web.host.name}:
${selenium.rc.port}/selenium-server/driver/?cmd=shutDown"
dest="result.txt" ignoreerrors="true" />
<echo taskname="selenium-shutdown"
message="Errors during shutdown are expected" />
</target>
</project>
執行 Selenium 測試時,當框架打開 Web 瀏覽器、閃電般執行測試,然后關閉該瀏覽器并生成 HTML 報告時,不要被嚇到。這是一種在開發生命周期的早期更快更容易地發現問題的方法(此時它們更易處理)。
使用 Cobertura 報告代碼覆蓋率
現在已經編寫了一些測試,如何確定所有這些測試執行什么 呢?幸運的是,此問題可由像 Cobertura 這樣的代碼覆蓋工具來解答。代碼覆蓋工具可報告測試覆蓋率 —— 以行覆蓋或分支覆蓋形式表示 —— 它表示測試運行時所涉及的代碼量。
清單 8 展示了一個 Ant 腳本。該腳本使用 Cobertura 生成一份關于代碼覆蓋率的 HTML 報告,代碼覆蓋率通過運行一系列 JUnit 測試獲得:
清單 8. 使用 Ant 和 Cobertura 報告代碼覆蓋率
<target name="instrument-classes">
<mkdir dir="${instrumented.dir}" />
<delete file="cobertura.ser" />
<cobertura-instrument todir="${instrumented.dir}">
<ignore regex="org.apache.log4j.*" />
<fileset dir="${classes.dir}">
<include name="**/*.class" />
<exclude name="**/*Test.class" />
</fileset>
</cobertura-instrument>
</target>
<target name="run-instrumented-tests" depends="instrument-classes">
<mkdir dir="${logs.junit.dir}" />
<junit fork="yes" haltonfailure="true" dir="${basedir}" printsummary="yes">
<sysproperty key="net.sourceforge.cobertura.datafile" file="cobertura.ser" />
<classpath location="${instrumented.dir}" />
<classpath location="${classes.dir}" />
<classpath refid="test.class.path" />
<classpath refid="project.class.path"/>
<formatter type="plain" usefile="true" />
<formatter type="xml" usefile="true" />
<batchtest fork="yes" todir="${logs.junit.dir}">
<fileset dir="${test.component.dir}">
<patternset refid="test.sources.pattern"/>
</fileset>
</batchtest>
</junit>
</target>
Cobertura 產生了一個如圖 1 中所示的 HTML 報告。請注意行覆蓋和分支覆蓋的百分比是以包計算的?蓡螕裘恳粋包,獲得類級別的行百分比和路徑百分比,甚至能看到執行的源代碼行和它們執行的次數。
圖 1. 使用 Cobertura 和 Ant 生成 HTML 報告
已經介紹了多種類型的測試,甚至介紹了如何測量這些測試的覆蓋率 —— 但是如何確保以正常的間隔執行 這些測試呢?恰好,這正是 CI 服務器(如 CruiseControl)大顯身手的地方,接下來對它進行介紹。
持續運行測試
一旦將這些各式各樣的開發人員測試類型合并到一個構建過程中時,可以將這些測試中的一些(或者全部)作為 CI 過程的一部分運行。例如,清單 9 是 CruiseControl 的 config.xml 文件的一個片段,我在其中定義了一些東西。首先,我讓 CruiseControl 每兩分鐘監控一次 Subversion 庫中的改變。如果發現任何改變,則 CruiseControl 將啟動一個叫做 build-${project.name}.xml 的委托 構建腳本(通常,此腳本用 Ant 編寫)。該委托構建腳本調用項目的構建腳本,后者執行編譯并運行測試。
我也定義了一些邏輯,將所有不同類型的測試結果合并到一個 CruiseControl 日志文件中。而且,我還利用 CruiseControl 的功能將不同工具生成的報告鏈接(使用 artifactspublisher 標簽)到 Build Artifacts 鏈接中,Build Artifacts 可以從 CruiseControl 的顯示板應用程序中獲得。
清單 9. 使用 CruiseControl 的 CI
...
<modificationset quietperiod="30">
<svn RepositoryLocation="http://your-domain.com/trunk/brewery"
username="bfranklin"
password="G0Fly@Kite"/>
</modificationset>
<schedule interval="120">
<ant anthome="apache-ant-1.6.5" buildfile="build-${project.name}.xml"/>
</schedule>
<log dir="logs/${project.name}">
<merge dir="projects/${project.name}/_reports/unit"/>
<merge dir="projects/${project.name}/_reports/component"/>
<merge dir="projects/${project.name}/_reports/performance"/>
<merge dir="projects/${project.name}/_reports/functional"/>
<merge dir="projects/${project.name}/_reports/coverage"/>
</log>
<publishers>
<artifactspublisher
dir="projects/${project.name}/_reports/"
dest="projects/artifacts/${project.name}"/>
</publishers>
...
在將每個源變更應用到版本控制庫中時,不必運行每個定義的測試。例如,可以設置 CI 系統執行構建(通常稱作提交構建),該構建只在代碼簽入時運行單元測試?梢詾樘峤粯嫿ㄑa充一些更重量級的構建,例如像運行組件測試、功能測試、性能測試以及甚至執行代碼檢查的構建(請參閱 參考資料)。這些構建可以以更低的頻率運行(如一天一次)。您也可以選擇在提交構建之后立即運行這些測試和檢查。
調用所有測試
持續測試包括了廣度和頻度。通過授權執行不同類型的測試,可獲得更大范圍的覆蓋和信任。此外,通過持續運行這些測試,幾乎能在問題產生就發現它們.僅僅進行單元測試(至少我所定義的單元測試),并不能使你在項目上走得很遠。取得更高的代碼覆蓋率并且增加團隊的信心,需要通力合作并執行自動化組件測試、性能測試和功能測試。此外,使用框架和像 JUnit、Selenium 以及 Cobertura 這樣的工具能輕松實現構建自動化,這也意味著在 CI 系統的幫助下,能夠在每次將變更提交到版本控制庫中時,有效地執行測試套件。這肯定是一種萬無一失的提高平均成功率的方法,您不這么認為嗎?
文章來源于領測軟件測試網 http://www.kjueaiud.com/