Selenium 是一种测试框架,它使您可在 Web 应用程序上轻松地运行用户验收测试(user acceptance test)。本月,Andrew Glover 将向您展示如何以编程的方式运行 Selenium 测试,并使用 TestNG 作为测试驱动程序。在将 TestNG 灵活的测试特性(包括参数化 fixture)添加到 Selenium 固有的工具包后,您需要做的就是借助 DbUnit 和 Cargo 的帮助编写完全自动化、逻辑可重复的验收测试。
Selenium 是一种 Web 测试框架,它搭建了验证 Web 应用程序的新途径。与大多数尝试模拟 HTTP 请求的 Web 测试工具不同,Selenium 执行 Web 测试时,就仿佛它本身就是浏览器。当运行自动的 Selenium 测试时,该框架将启动一个浏览器,并通过测试中描述的步骤实际驱动浏览器,用户将使用这种方式与应用程序交互。
由于开发人员和非开发人员都能够使用 Selenium 轻松地编写测试,使得它从众多测试框架应用程序中脱颖而出。在 Selenium 中,可以通过编程的方式编写测试,或者使用 Fit 样式的表,并且编写了测试后,可以使测试完全自动化。使用一个 Ant 构件(比方说)运行完整的 Selenium 套件非常简单,并且还可以在持续集成(Continuous Integration,CI)环境中运行 Selenium 测试。
这个月,我将介绍 Selenium,并逐一查看使它成为优秀 Web 测试框架的一些特性 —— 尤其是在结合使用 TestNG、DbUnit 和 Cargo 这样的软件时。
|
在 Selenium 中,您可以使用自己喜爱的语言或者 Fit 样式的表通过编程来编写测试。从测试的角度来说,不管使用什么语言,测试过程和结果都不会有显著的差别。在此,我希望研究 Selenium 的编程方法,因为在结合使用 TestNG 时,它提供了一些有趣的可行方法能性。
使用具有类似 TestNG 这样的框架的 Selenium 进行编程式测试具有这样一个优点,它允许您创建智能 fixture,而使用 Fit 样式的表则很难做到这一点。TestNG 尤其适合与 Selenium 结合使用,因为它使您能够完成其他框架无法做到的测试,例如使用依赖项进行测试,重新运行失败了的测试,以及使用单独文件中定义的参数进行参数化测试。所有这些特性结合在一起,当然能够使它在众多 Web 应用程序测试框架中脱颖而出,但是,正如您将看到的,在完全自动化的验收测试中使用这些特性令它更加出众。
Selenium 架构实际上由两个逻辑实体组成:您编写的代码以及能够简化与测试中的应用程序的交互的 Selenium 服务器。要成功地执行测试,必须要启动并运行 Selenium 服务器实例以及要测试的应用程序。(当然,测试结果取决于您编写的应用程序是否优秀!)
幸运的是,Selenium 服务器是一种轻量级程序,可以在实际的测试范围内通过编程启动和停止它。Selenium 服务器(使用 Selenium
对象嵌入)的启动和停止由一个 fixture 来执行。
要通过编程的方式启动 Selenium 服务器,必须创建一个新的 Selenium
对象,并告诉它要使用哪一种兼容的浏览器 —— 我在下面的示例中使用的是 Firefox。您还必须提供运行服务器实例的位置(通常是 localhost
,但不是必须的),以及被测试的应用程序使用的基 URL。
在清单 1 中,我配置了一个本地 Selenium
实例,使用它在本地安装的 Web 应用程序上驱动 Firefox(http://localhost:8080/gt15/
)。正如您从参数中推断的一样,Selenium
是作为被测试的应用程序的代理,并相应地促进测试。
Selenium driver = new DefaultSelenium("localhost", SeleniumServer.getDefaultPort(), "*firefox", "http://localhost:8080/gt15/");driver.start();//go to web pages and do stuff...driver.stop(); |
创建了 Selenium
实例后,您可以 启动
并在运行时 停止
它。这意味着您可以通过编程与 Selenium 服务器交互,并通过一个测试程序使它驱动浏览器。
|
通过编程与 Web 页面进行交互是一种使用本地 id 的应用。(一些读者可能对这种源自 本系列二月份关于 TestNG-Abbot 的文章 的概念比较熟悉)。与页面元素进行交互的第一步就是查找该元素,通常可以使用 HTML 元素 ID 进行查找。Selenium 还允许您使用 XPath、正则表达式,甚至是 JavaScript 来查找特定的元素(如果您希望这样做)。
清单 2 所示的 HTML 是使用 Groovlet 的简单 Web 应用程序的一部分。这段代码定义了包含输入和提交按钮的表单。如果希望 Selenium 与该表单交互,我必须为输入按钮提供 ID 以及相应的值。我还需要为提交按钮提供一个 ID,这样 Selenium 才能 “单击” 它。单击按钮后,表单将被提交给 Groovlet —— 本例中为 FindWidget.groovy
。
<form method=post action="./FindWidget.groovy"> <table border="0" style="border-style: dotted"> <tr> <td class="heading">Widget:</td> <td class="value"><input type="text" name="widget"></td> </tr> <tr> <td></td> <td class="value"><input type="submit" value="Find Description" name="submit"></td> </tr> </table></form> |
现在就可以通过使用 ID widget
(输入值)和 submit
(单击按钮)与该 HTML 表单进行编程式交互,如清单 3 所示:
driver.type("widget", "pg98-01"); driver.click("submit");driver.waitForPageToLoad("10000");//assert some return value... |
Selenium 中用于和 Web 页面元素进行交互的 API 非常的直观。对于输入字段,我可以使用 type()
方法将值与 ID 关联起来。如果需要的话,可以通过编程 click
按钮。在清单 3 中,我将 click
设置为 10 秒的等待时间 —— 足够表单提交请求完成处理。当 FindWidget.groovy
中的代码运行其内容并返回响应后,我可以使用该响应来查找特定页面元素,并验证所有内容是否正常工作。
|
TestNG 以其灵活性和参数化 fixture 成为定义 Selenium 的驱动验收测试的首选。TestNG 能够定义测试依赖项并返回失败的测试,以及其易用性,使得 Selenium-TestNG 成为吸引人的组合。
让我们首先从一个能够允许用户创建、查找、更新或删除小部件的 Web 应用程序开始。创建一个小部件需要三个属性:名称、类型和定义。图 1 显示了创建小部件的表单:
请注意:表单元素的类型是具有三个不同选项的下拉列表,如图 2 所示:
单击 Create Widget 将促使 Groovlet 处理这一请求。如果所有内容正确的话(即名字和定义不为空,并且数据库中不存在该实例),Groovlet 将创建一个新的小部件实例并类似图 3 所示的状态页面:
结合使用 Selenium 和 TestNG 验证简单的 Create Widget 用例是一种可管理的应用:
请注意:用例中的每一步都是通过 Selenium 完成的 —— 所以说,TestNG 仅仅帮助进行查找。现在,我们来实践一下。
|
我希望对 Selenium 服务器进行灵活的配置,所以我将编写一个参数化 fixture(TestNG-Selenium 样式),一般可以使用它来为不同浏览器、不同位置甚至混合的 Web 应用程序地址(类似 localhost
和产品)创建 Selenium 服务器。清单 4 定义了我所配置的灵活的 Selenium 服务器 fixture:
@Parameters({"selen-svr-addr","brwsr-path","aut-addr"}) @BeforeClass private void init(String selenSrvrAddr, String bpath, String appPath) throws Exception { driver = new DefaultSelenium(selenSrvrAddr, SeleniumServer.getDefaultPort(), bpath, appPath); driver.start(); } //.... @AfterClass private void stop() throws Exception { driver.stop(); } |
必须将参数名与 TestNG 的 testng.xml 文件中的值链接起来;因此,我定义了如清单 5 所示的三个参数。(默认情况下为 Firefox 定义了 brwsr-path
参数,但是我可以同样轻松地定义一组新的使用 Internet Explorer 的测试。)
<parameter name="selen-svr-addr" value="localhost"/> <parameter name="aut-addr" value="http://localhost:8080/gt15/"/> <parameter name="brwsr-path" value="*firefox"/> |
接下来,我将定义清单 6 所示的测试用例,它也包含一个参数,用于进行测试的应用程序的基 URL。该测试将促使浏览器在 Web 应用程序内打开特定页面,并操作 图 1 所示的表单。
@Parameters({"aut-addr"}) @Test public void verifyCreate(String appPath) throws Exception { driver.open(appPath + "/CreateWidget.html"); driver.type("widget", "book-01"); driver.select("type", "book"); driver.type("definition", "book widget type book"); driver.click("submit"); driver.waitForPageToLoad("10000"); assertEquals(driver.getText("success"), "The widget book-01 was successfully created.", "test didn't return expected message"); } |
通过调用 driver.click("submit")
提交表单后,Selenium 将等待响应的加载,然后我将断言成功的创建信息。(注意:响应 Web 页面具有一个 ID 为 success 的元素。)
结果产生一个灵活的文本类,它将检验两种场景:一种是良好的场景,而另一种是没有提供定义的边界用例,如清单 7 所示:
public class CreateWidgetUATest { private Selenium driver; @Parameters({"selen-svr-addr","brwsr-path","aut-addr"}) @BeforeClass private void init(String selenSrvrAddr, String bpath, String appPath) throws Exception { driver = new DefaultSelenium(selenSrvrAddr, SeleniumServer.getDefaultPort(), bpath, appPath); driver.start(); } @Parameters({"aut-addr"}) @Test public void verifyCreate(String appPath) throws Exception { driver.open(appPath + "/CreateWidget.html"); driver.type("widget", "book-01"); driver.select("type", "book"); driver.type("definition", "book widget type book"); driver.click("submit"); driver.waitForPageToLoad("10000"); assertEquals(driver.getText("success"), "The widget book-01 was successfully created.", "test didn't return expected message"); } @Parameters({"aut-addr"}) @Test public void verifyCreationError(String appPath) throws Exception { driver.open(appPath + "/CreateWidget.html"); driver.type("widget", "book-02"); driver.select("type", "book"); //definition explicitly set to blank driver.type("definition", ""); driver.click("submit"); driver.waitForPageToLoad("10000"); assertEquals(driver.getText("failure"), "There was an error in creating the widget.", "test didn't return expected message"); } @AfterClass private void stop() throws Exception { driver.stop(); }} |
目前为止,我已经定义了两种足够灵活的 Selenium 测试,可以对多个浏览器进行测试,并且还可以对多个位置进行测试,这对初学者非常有利。尽管如此,我还想获得更高级点的应用,我开始考虑测试中的逻辑是否可重复使用。比如,如果对一行运行两次 CreateWidgetUATest
测试类会怎样?如何确保我的 Web 应用程序运行的是本地机器(或其他机器)上最新版本的代码?
|
在执行 Selenium 测试时,必须运行 Selenium 服务器以及要检验的 Web 应用程序。言外之意,还必须运行应用程序中所有相关的架构依赖关系 —— 对于大多数 Java™ Web 应用程序来说,即 Servlet 容器和相关的数据库。
正如在我的另一篇文章 repeatable system tests 中解释的一样,DbUnit 和 Cargo 是两种我最喜欢的技术,可以在依赖数据库的 Web 应用程序中实现逻辑重复。DbUnit 管理数据库中的数据,而 Cargo 使容器管理以通用的方式实现自动化。下面几节将向您展示如何结合使用 Selenium 和 TestNG 从而确保实现逻辑重复的验收测试。
|
您可能回想起,DbUnit 通过有效地管理测试场景中的数据简化了使用数据库的工作。通过使用 DbUnit,可以在测试前将一组已知的数据加载到数据库中,这意味着您可以依赖这些在测试过程中呈现的数据。此外,在完成测试后,还可以从数据库中删除测试结果产生的数据。DbUnit 作为一种方便的 fixture(JUnit 或 TestNG)简化了所有这些工作,它能够读取包含测试数据的种子文件,逻辑插入、删除数据,或更新数据到相应的数据库表中。
由于这里使用了 TestNG 驱动 Selenium,我将创建一个 DbUnit fixture,它将在测试 级别上运行。TestNG 支持在五种粒度级别上运行 fixture。最低的两种级别,方法和类是最常见的 —— 用于每个测试方法的 fixture 或者用于整个类的 fixture。之后,TestNG 为一个测试集合(定义在 TestNG 配置文件中并由 test
元素指定)定义了一个 fixture,为一组 测试(定义在 TestNG 的 Test
注释中)定义了一个 fixture。
创建一个 DbUnit fixture 并在测试级别上运行,这意味着运行任何测试之前,测试类的集合将共享相同的逻辑,为数据库正确地播种。在本文的示例中,在运行每个逻辑测试集合前,我希望数据库具有一组干净的数据。使用 DbUnit 的 CLEAN_INSERT
命令确保在先前运行的测试中创建的行被删除掉 —— 因此,我可以重新运行测试,该测试可以不断创建数据并且不用考虑数据库约束。
此外,我希望 fixture 能够依赖参数化数据,这使我在运行某个测试之前,能够灵活地切换种子文件,甚至是特定数据库的位置。将 TestNG 与参数相关联起来再简单不过了:我所需做的仅仅是使用 Parameters
注释装饰 fixtrue,声明方法签名中相应的参数,并提供 TestNG 配置文件中的值。
清单 8 定义了一个简单的 DbUnit fixture,它使用所需的种子文件播种数据库。请注意:该 fixture 被定义为包含五个 参数。(这可能非常多,但是在 fixture 中包含参数不是很好吗?)
public class DatabaseFixture { @Parameters({"seed-path","db-driver","db-url","db-user","db-psswrd"}) @BeforeTest public void seedDatabase(String seedpath, String driver, String url, String user, String pssword) throws Exception { IDatabaseConnection conn = this.getConnection(driver, url, user, pssword); IDataSet data = this.getDataSet(seedpath); try { DatabaseOperation.CLEAN_INSERT.execute(conn, data); }finally { conn.close(); } } private IDataSet getDataSet(String path) throws IOException, DataSetException { return new FlatXmlDataSet(new File(path)); } private IDatabaseConnection getConnection(String driver, String url, String user, String pssword ) throws ClassNotFoundException, SQLException { Class.forName(driver); Connection jdbcConnection = DriverManager.getConnection(url, user, pssword); return new DatabaseConnection(jdbcConnection); }} |
要将实际的值与清单 8 中的参数相关联,我必须在 TestNG 的 testng.xml
文件中定义它们,如清单 9 所示:
<parameter name="seed-path" value="test/conf/gt15-seed.xml"/> <parameter name="db-driver" value="org.hsqldb.jdbcDriver"/> <parameter name="db-url" value="jdbc:hsqldb:hsql://127.0.0.1"/> <parameter name="db-user" value="sa"/> <parameter name="db-psswrd" value=""/> |
现在我已经定义了一个灵活的 fixture,它将处理数据库状态和相应测试。现在可以准备使用 TestNG 将所有内容连接起来。通常,第一步是了解希望实现的内容。在本例中,我想完成以下任务:
TestNG 的 parameter
元素的作用域是局部的,这对我来说是件好事。这样,我可以很容易地在 TestNG 配置文件中定义通用参数值,并且当需要时在 TestNG 的 test
组元素中重写它们。
比如,要运行两组测试,简单创建两个 test
元素。我可以通过 TestNG 的 package
元素将我的 fixture 和相关测试包括进来,package
元素能够使包结构中所有测试(或 fixture)的查找变得简单。接着,我可以在两个定义了的 test
组中将 Firefox 和 Internet Explorer 的 brwsr-path
参数关联起来。所有这些都显示在了 testng.xml 文件中,如清单 10 所示:
<suite name="User Acceptance Tests" verbose="1" > <!-- required for DbUnit fixture --> <parameter name="seed-path" value="test/conf/gt15-seed.xml"/> <parameter name="db-driver" value="org.hsqldb.jdbcDriver"/> <parameter name="db-url" value="jdbc:hsqldb:hsql://127.0.0.1"/> <parameter name="db-user" value="sa"/> <parameter name="db-psswrd" value=""/> <!-- required for Selenium fixture --> <parameter name="selen-svr-addr" value="localhost"/> <parameter name="aut-addr" value="http://localhost:8080/gt15/"/> <test name="GT15 CRUDs- Firefox" > <parameter name="brwsr-path" value="*firefox"/> <packages> <package name="test.com.acme.gt15.Web.selenium" /> <package name="test.com.acme.gt15.Web.selenium.fixtures" /> </packages> </test> <test name="GT15 CRUDs- IE" > <parameter name="brwsr-path" value="*iexplore"/> <packages> <package name="test.com.acme.gt15.Web.selenium" /> <package name="test.com.acme.gt15.Web.selenium.fixtures" /> </packages> </test></suite> |
我很高兴地宣布,我已经完成了创建一套可重复验收测试所需的所有事情。剩下的工具就是处理 Web 应用程序容器本身。幸运地是,我可以使用 Cargo 来完成。
|
Cargo 是一个创新的以通用方式自动化容器管理的开源项目,比如,用于将 WAR 文件部署到 JBoss 的相同 API 还可以启动和停止 Tomcat。Cargo 还可以自动下载并安装容器 —— Cargo API 的用途很广泛,从 Java 代码到 Ant 任务,甚至是 Maven。
诸如 Cargo 这样的工具将处理编写逻辑重复测试用例所面对的一个大的挑战,它避免一种潜在的假设,即运行 的容器具有最新最好的应用程序代码。此外,还可以构造一个利用 Cargo 的能力自动完成以下任务的编译过程(例如在 Ant 内):
稍后,您还可以使 Cargo 停止所选的容器。(并且,不需要对下载和安装容器发出警告,或者,如果本地机器中已经存在了正确的版本,Cargo 将跳过步骤 1 和 2。)
我希望使用 Cargo 来确保启动并运行最新和最好的 Web 应用程序。并且,我不需要考虑在哪里部署 WAR 文件,或者必须确保正在使用的是最新的 WAR 文件。我真正想达到的目的是使用户验收测试实现无事件 —— 我仅需要发出一个 命令,然后坐下来等待结果。甚至可以更好,在一个 CI 环境中,我不用等待;当测试完成后我将获得一个通知!
要在 Ant 内设置 Cargo,我需要定义一个任务,它将下载特定版本的 Tomcat 并将其安装到本地机器上的临时目录。接下来,将最新版本的代码部署到 Tomcat 上,如清单 11 所示:
<target name="ua-test" depends="compile-tests,war"> <taskdef resource="cargo.tasks"> <classpath> <pathelement location="${libdir}/${cargo-jar}" /> <pathelement location="${libdir}/${cargo-ant-jar}" /> </classpath> </taskdef> <cargo containerId="tomcat5x" action="start" wait="false" id="${tomcat-refid}"> <zipurlinstaller installurl="${tomcat-installer-url}" /> <configuration type="standalone" home="${tomcatdir}"> <property name="cargo.remote.username" value="admin" /> <property name="cargo.remote.password" value="" /> <deployable type="war" file="${wardir}/${warfile}" /> </configuration> </cargo> <antcall target="_start-selenium" /> <cargo containerId="tomcat5x" action="stop" refid="${tomcat-refid}" /></target> |
清单 11 中的 target 使用 antcall
调用另一个 target。实际上,清单 11 中最后的 cargo
任务封装了 _start-selenium
target,并且确保运行测试后停止 Tomcat。
在清单 12 中定义的 _start-selenium
target 中,我需要启动(并稍后停止)Selenium 服务器。在此过程中,我的测试还将连接到其 Selenium fixture 中的服务器实例。请注意:该 target 是如何引用另一个 target ——
<target name="_start-selenium"> <java jar="${libdir}/${selenium-srvr-jar}" fork="true" spawn="true" /> <antcall target="_run-ua-tests" /> <get dest="${testreportdir}/results.txt" src="${selenium-srvr-loc}/selenium-server/driver/?cmd=shutDown" /></target> |
最后,该组中最后的 target 将通过 TestNG 实际运行我的编程式 Selenium 测试。注意,我是如何通过使用清单 13 中的 _run-ua-tests
target 的 xmlfileset
元素,使 TestNG 使用我的 testng.xml 文件。
<target name="_run-ua-tests"> <taskdef classpathref="build.classpath" resource="testngtasks" /> <testng outputDir="${testreportdir}" classpath="${testclassesdir};${classesdir}" haltonfailure="true"> <xmlfileset dir="./test/conf" includes="testng.xml" /> <classpath> <path refid="build.classpath" /> </classpath> </testng></target> |
|
正如您看到的一样,Selenium 极大地简化了用户验收测试,尤其当使用 TestNG 驱动的时候。虽然编程式测试并不适用于所有人(非开发人员可能更喜欢 Selenium 的 Fit 样式的表),它确实让您了解到了 TestNG 非凡的灵活性。编程式测试还允许您使用 DbUnit 和 Cargo 构建自己的测试框架,从而确保测试的逻辑可重复性。
开源 Web 测试框架的发展绝不会停止,这对于追求代码质量的完美主义者是个好消息。Selenium 是驱动浏览器的开源 Web 测试框架中新出现的工具之一,它能够使用户验收测试自动化 —— 因此,它非常优秀。结合使用 Selenium 和 TestNG,正如我在本文中演示的一样,您将获得一个非常好的测试驱动,并从依赖性测试以及参数测试中获得巨大的优势。尝试使用 Selenium 和 TestNG 吧,您的用户将为此感谢您。