单元测试框架,比如 JUnit,允许您测试在您的服务器上运行的代码。但是,在一个典型 web 应用程序中,服务代码只是应用程序中的全部代码的一小部分。这样的应用程序也可能拥有大量代码,只有使用一个使用浏览器来测试应用程序的工具才能测试这样巨大的代码量。
测试 web 应用程序更困难的一个方面是测试应用程序 UI — 应用程序的这个代码部分通常从 HTML 和 JavaScript 代码生成。UI 在浏览器中、而不是在服务器进程中运行,因此只能从一个 Internet 浏览器来测试它。这种类型的代码示例包括 JavaServer Pages (JSP) 页面、PHP 代码和 Ruby。
本文介绍社区中的一个工具 — Selenium — 它可用于创建和自动化 web 测试。您将了解到如何快速创建一些样例测试,如何扩展它们,以及如何使用 Selenium Remote Control 来将它们作为自动测试运行。
了解 Selenium RC
Selenium RC 是来自 Selenium 项目的一个工具套件的一部分。它允许您运行您创建的自动化测试。Selenium RC 支持许多不同的操作系统,能够启动不同浏览器的实例,比如 Windows® Internet Explorer®、Mozilla Firefox 和 Opera。
使用 Selenium RC,您可以随意多次自动运行测试。这个工具还支持您创建比单独使用 Selenium IDE 创建的测试更复杂的测试。您可以添加条件语句和迭代以进行测试,如果您想使用一个数据集运行测试,这个功能就会有所帮助。您还可以使用 JUnit 这样的单元测试框架中可用的构件来处理预期的异常。
下载和安装 Selenium RC
要开始使用 Selenium RC,首先必须下载并安装它。Selenium 服务器只是一个 JAR 文件,可以使用 Java™ Runtime Environment (JRE) 运行。您可以从 SeleniumHQ 站点(参见 参考资料 中的链接)下载这个 JAR 文件和其他支持文件。
下载完成后,解压缩包含 Selenium RC 的存档文件,将其保存到一个位置,您需要记住这个位置,以便稍后使用。可以通过运行以下命令来执行 Selenium 服务器:
java -jar selenium-server.jar
|
要测试的样例应用程序
本文使用一个简单的动态 web 应用程序,借助一些 JSP 页面来展示 Selenium RC 如何工作,以及如何用于自动化测试。这个简单的 web 应用程序拥有两个页面:一个登录页面和一个输入页面;后者用于输入您的姓名和生日等信息,应用程序从该页面计算您的年龄并说 “Hello”。
这个样例应用程序提供了一个机会来展示如何执行一些测试来处理不同的情况。第一个页面如清单 1 所示。这是一个简单的登录页面,允许您登录到这个 web 应用程序。为了简化这个示例,登录信息将与一些硬编码的字符串进行比较。
清单 1. index.jsp 页面
01 |
< a rel = "nofollow" >< strong ><%@ page language="java" contentType="text/html; charset=UTF-8"
|
02 |
pageEncoding="UTF-8"%>
|
05 |
if ("process".equals(request.getParameter("action")))
|
07 |
if ("user".equals(request.getParameter("username")) &&
|
08 |
"secret".equals(request.getParameter("password")))
|
10 |
response.sendRedirect("enterInfo.jsp");
|
16 |
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" |
17 |
"http://www.w3.org/TR/html4/loose.dtd">
|
20 |
< meta http-equiv = "Content-Type" content = "text/html; charset=UTF-8" >
|
21 |
< title >Test Login Page</ title >
|
24 |
< form action="<%= request.getRequestURI() %>" method="POST">
|
25 |
< input type = "hidden" value = "process" name = "action" />
|
26 |
< label for = "username" >Username:< br />
|
27 |
< input type = "text" id = "username" name = "username" />
|
30 |
< label for = "password" >Password:< br />
|
31 |
< input type = "password" id = "password" name = "password" />
|
34 |
< input type = "submit" value = "Login" />
|
第二个页面如清单 2 所示。当您输入您的名和生日时,该页面将说 “Hello” 并告知您今天您的年龄。这个示例有一点勉强,因为在大多数 web 应用程序中,这样的信息将需要一个登录,数据将被存储以便下次登录使用。
清单 2. enterInfo.jsp 页面
01 |
< a rel = "nofollow" ><%@ page language="java" contentType="text/html; charset=UTF-8"
|
02 |
pageEncoding="UTF-8"%>
|
03 |
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" |
04 |
"http://www.w3.org/TR/html4/loose.dtd">
|
05 |
<%@page import="java.util.Date"%> |
06 |
<%@page import="java.text.SimpleDateFormat"%> |
07 |
<%@page import="java.text.ParseException"%> |
10 |
private Date parseDate(String dateValue) |
12 |
Date returnDate = null;
|
16 |
// this is not the most efficient of methods, and try...catch
|
17 |
// statements ideally should not be used as business exception
|
19 |
SimpleDateFormat dateFormat = new SimpleDateFormat("MM/dd/yyyy");
|
20 |
returnDate = dateFormat.parse(dateValue);
|
22 |
} catch (ParseException pe)
|
33 |
< meta http-equiv = "Content-Type" content = "text/html; charset=UTF-8" >
|
34 |
< title >Insert title here</ title >
|
37 |
< form action="<%= request.getRequestURI() %>" method="POST">
|
38 |
< input type = "hidden" value = "process" name = "action" />
|
39 |
< label for = "name" >Your name:< br />
|
40 |
< input type = "text" id = "name" name = "name" />
|
43 |
< label for = "birthdate" >Your birth date (in MM/DD/YYYY format):< br />
|
44 |
< input type = "text" id = "birthdate" name = "birthdate" />
|
49 |
if ("process".equals(request.getParameter("action")))
|
52 |
Date birthDate = parseDate(request.getParameter("birthdate"));
|
54 |
if (birthDate == null)
|
56 |
// display the error messages...
|
57 |
out.write("< p class=\"error\">Please enter a valid date.</ p >");
|
59 |
// display the nice messages...
|
65 |
< input type = "submit" value = "Submit" />
|
编写第一个测试
要快速编写一些测试以用作您的自动化测试的基础,可以从 Selenium IDE 入手。Selenium IDE 是一个允许录制测试的 Firefox 插件。然后,您可以导出这些已录制的测试,以便给它们添加一些 “装饰” — 条件、迭代等。
要开始使用 Selenium IDE,首先需要从 参考资料 中的链接下载它。通过单击该链接将其安装为一个 Firefox 插件。Firefox 将提示您安装这个插件,然后重新启动浏览器,以便更改生效。
安装这个插件后,启动您的服务器,以便开始使用您的 web 应用程序。在 Firefox 中单击 Tools > Selenium IDE,打开 Selenium IDE。Selenium IDE 打开后,单击 Record。Selenium 开始录制后,它将记住您在浏览器中执行的所有动作。要登录您的样例应用程序,执行以下步骤:
- Selenium IDE 录制的同时,导航到 index.jsp web 页面。
- 输入您的用户名。
- 在 Password 框中输入有效密码。
- 单击 Login。
成功登录后,您的 web 应用程序将转到 enterInfo.jsp 页面。此时,您的测试中有一些动作,但目前为止,无法验证您的动作是否有效。Selenium 需要知道应该寻找的内容,以便知道您的 enterInfo.jsp 页面正在按预期的方式呈现。
您可以添加一些验证动作,以确保您的应用程序正在显示正确的内容。当 Selenium 在录制时,执行以下步骤:
- 右键单击一个 HTML 元素,比如
Your name
标签,然后单击 verifyTextPresent Your name:。
- 对
Your birth date
标签重复步骤 1。
- 单击 Record 停止录制。
如果您想查看您的测试的运行情况,单击 Run All(参见图 1)。测试将执行并通过。
图 1. 单击 Run All
要查看 Selenium IDE 是否真的在测试您的应用程序,转到您的 IDE 并更改有效的用户名的值。重新部署您的应用程序,然后再次单击 Run All。这次,您的测试将失败,因为 web 应用程序没有显示带有正确标签的 enterInfo.jsp 页面。
将您的测试导出到 JUnit
现在您已经录制了您的第一个测试,现在可以导出它以便在 JUnit 中使用。在 Selenium IDE 中突出显示您的测试,单击 File > Export Test Case As > Java (JUnit) - Selenium RC。键入您的测试的名称(例如,IndexTests.java),将它保存到一个位置,记住该位置,以便稍后将其导入 Eclipse。
执行以下步骤:
- 创建一个新的 Java 项目,该项目包含一些使用 JUnit 构建的 Java 单元测试。
- 下载 Selenium RC 二进制文件(参见 参考资料 中的链接)。保存这个存档文件,以便稍后将其中的文件在 Eclipse 中导入您的新 Java 项目。
- 在您的 Java 项目中创建一个名为 lib 的新文件夹。
- 单击 File > Import,然后单击列表中的 File System。 单击 Next。
- 浏览到您刚才解压 Selenium RC 文件的位置,选择 selenium-java-client-driver-1.0.1 目录。
- 从列表中选择 selenium-java-client-driver.jar,单击 OK 将 Selenium Java 客户端驱动程序 JAR 文件导入您的项目的 lib 目录。
- 将您从 Selenium IDE 导出的 Java 文件导入您的新 Java 项目的 src 目录。
您将注意到的第一件事是:您将得到许多编译错误。首先,您的新 Java 文件也许不在正确的包中,因为它直接位于 src 文件夹中。其次,JUnit 或 Selenium 类无法找到。幸运的是,这些错误可以使用 Eclipse 轻松解决。
在 Eclipse 中解决错误
要解决包错误,单击该错误,然后按下 Ctrl-1 打开提示。选择 Move MyTest.java to package 'com.example.mywebapp.tests'。 Eclipse 将为您创建包结构并移动文件。
添加 JUnit 测试
现在我们来添加 Selenium 和 JUnit 测试,在 Package Explorer 单击 Java 项目,然后在 Libraries选项卡上单击 Build Path > Configure Build Path,单击 Add JARs,选择 selenium-java-client-driver.jar 文件。单击 OK。
当 Java 源文件被编译时,它应该如清单 3 所示。
清单 3. 基于从 Selenium IDE 导出的代码的 JUnit 测试
01 |
<a rel= "nofollow" ><strong> package com.example.mywebapp.tests;
|
03 |
import com.thoughtworks.selenium.*;
|
04 |
import java.util.regex.Pattern;
|
06 |
import org.junit.BeforeClass;
|
07 |
import org.junit.Test;
|
09 |
public class MyTest extends SeleneseTestCase {
|
12 |
public void setUp() throws Exception {
|
13 |
setUp( "http://localhost:8080/tested-webapp/index.jsp" , "*chrome" );
|
17 |
public void testMy() throws Exception {
|
18 |
selenium.open( "/tested-webapp/index.jsp" );
|
19 |
selenium.type( "username" , "user" );
|
20 |
selenium.type( "password" , "secret" );
|
21 |
selenium.click( "//input[@value='Login']" );
|
22 |
selenium.waitForPageToLoad( "30000" );
|
23 |
verifyTrue(selenium.isTextPresent( "Your name:" ));
|
24 |
verifyTrue(selenium.isTextPresent( "Your birth date (in MM/DD/YYYY format):" ));
|
如果有一些限制阻止您使用 Firefox,无法下载和安装 Selenium IDE,您仍然可以编写使用 Selenium RC 运行的测试。尽管使用这里展示的示例作为一个开端并使用 参考资料 中提供的 Selenium 文档。
查看测试结果
您已经编写了第一个示例测试,现在可以通过启动 Selenium 服务器并将该测试作为一个标准 JUnit 单元测试运行,查看测试的实际运行情况。通过运行以下命令启动 Selenium 服务器:
java -jar selenium-server.jar
|
Selenium 服务器启动后,右键单击 IndexTest.java 文件,然后单击 Run As > JUnit Test,运行这个单元测试。Selenium 服务器启动您的浏览器的一个实例并运行这个测试可能需要一些时间。测试完成后,Eclipse 中显示的输出应与常规单元测试相同。
深入挖掘
您已经使用 Selenium IDE 创建了一个简单测试并将其导出为一个 Java 文件,现在我们创建一个更复杂的测试来验证 enterInfo.jsp 页面的功能。这个示例测试如清单 4 所示。
清单 4. 测试 enterInfo.jsp 的 JUniti 测试
01 |
package com.example.mywebapp.tests;
|
03 |
import org.junit.BeforeClass;
|
04 |
import org.junit.Test;
|
06 |
import com.thoughtworks.selenium.SeleneseTestCase;
|
08 |
public class EnterInfoTests extends SeleneseTestCase {
|
11 |
public void setUp() throws Exception {
|
12 |
setUp( "http://localhost:8080/tested-webapp/index.jsp" , "*firefox" );
|
16 |
public void testBadDate() {
|
18 |
selenium.type( "name" , "User" );
|
19 |
selenium.type( "birthdate" , "@#$#@" );
|
20 |
selenium.click( "//input[@value='Submit']" );
|
21 |
selenium.waitForPageToLoad( "30000" );
|
22 |
verifyTrue(selenium.isTextPresent( "Please enter a valid date" ));
|
26 |
public void testValidDate() {
|
28 |
selenium.type( "name" , "User" );
|
29 |
selenium.type( "birthdate" , "12/2/1999" );
|
30 |
selenium.click( "//input[@value='Submit']" );
|
31 |
selenium.waitForPageToLoad( "30000" );
|
32 |
verifyFalse(selenium.isTextPresent( "Please enter a valid date" ));
|
35 |
private void doLogin()
|
37 |
selenium.open( "/tested-webapp/index.jsp" );
|
38 |
selenium.type( "username" , "user" );
|
39 |
selenium.type( "password" , "secret" );
|
40 |
selenium.click( "//input[@value='Login']" );
|
41 |
selenium.waitForPageToLoad( "30000" );
|
这个示例测试使用 清单 3 中显示的 LoginTest
类作为一个出发点。doLogin()
函数拥有使您登录到一个应用程序的 代码,这个应用程序用于在测试开始时将您带到适当的点。
testBadDate()
方法的作用是在表单上的 birthdate 域中输入错误的值,然后提交该值。测试证明,如果日期不正确,将显示适当的错误消息。testValidDate()
方法测试表单上的有效日期,确保提供用户年龄的消息是正确的。
使用 JUnit 中可用的 Java 技术的威力,您可以循环一些示例,向您的测试添加一些条件,并预见异常。
自动化测试
借助一个 Apache Ant 脚本和几个目标的帮助,您已经在 Eclipse IDE 中运行了一些测试,您还可以完全自动化这些测试。拥有这个 Ant 脚本后,您可以使用 Hudson 或 CruiseControl 等工具来持续运行这些测试。
要自动化测试,可以使用一个采用 JUnit 目标的 Ant 脚本来执行一些 Selenium RC 测试。这个脚本如清单 5 所示。
清单 5. 运行 Selenium 测试的 Ant 脚本
01 |
< project name = "tested-webapp-tests" default = "run-tests" basedir = "." >
|
03 |
< property name = "selenium.server.jar" value = "path/to/selenium-server.jar" />
|
04 |
< property name = "src" value = "${basedir}/src" />
|
05 |
< property name = "build" value = "${basedir}/bin" />
|
06 |
< property name = "lib" value = "${basedir}/lib" />
|
09 |
< fileset dir = "${lib}" includes = "**/*.jar" />
|
12 |
< target name = "start-selenium-server" >
|
13 |
< java jar = "${selenium.server.jar}" fork = "true" spawn = "true" >
|
14 |
< arg line = "-timeout 30" />
|
18 |
< target name = "compile-tests" >
|
19 |
< javac srcdir = "${src}" destdir = "${build}" fork = "true" />
|
22 |
< target name = "run-tests" depends = "compile-tests" >
|
23 |
< junit printsummary = "yes" >
|
25 |
< path refid = "classpath" />
|
28 |
< batchtest fork = "yes" >
|
29 |
< fileset dir = "${src}" >
|
30 |
< include name = "**/*Tests.java" />
|
34 |
< echo message = "Finished running tests." />
|
37 |
< target name = "stop-selenium-server" >
|
38 |
< get taskname = "selenium-shutdown"
|
39 |
src = "http://localhost:4444/selenium-server/driver/?cmd=shutDown"
|
41 |
ignoreerrors = "true" />
|
44 |
< target name = "run-all" >
|
46 |
< antcall target = "start-selenium-server" >
|
49 |
< echo taskname = "waitfor" message = "Wait for proxy server launch" />
|
50 |
< waitfor maxwait = "1" maxwaitunit = "minute" checkevery = "100" >
|
51 |
< http url="http://localhost:4444/selenium-server/
|
52 |
driver/? cmd = testComplete " />
|
54 |
< antcall target = "run-tests" >
|
56 |
< antcall target = "stop-selenium-server" >
|
这个 Ant 脚本有 3 个目标。第一个目标 start-selenium-server
以 分叉(forked)和派生(spawned)模式运行 Selenium 服务器,以便它在后台运行。run-tests
目标实际执行 JUnit 测试,stop-selenium-server
目标通过调用一个向服务器发送一条 shutdown 命令的 URL 来停止服务器。
使用这样一个 Ant 脚本,可以以一种自动化方式运行一些 Selenium 测试,方法是计划这些测试,或者在一个持续集成工具中使用这个 Ant 脚本。
在其他浏览器中运行测试
目前为止,这些测试被编写为在 Firefox 中运行。但是,有时候需要在其他浏览器中测试这个 web 应用程序,以确保它能跨浏览器正常工作。
您可能已经注意到,当 Selenium 测试在 JUnit 测试的 setUp()
方法中得到设置时,*chrome
作为父节点的 setUp()
方法的第二个参数传递。这个参数启动一个 Firefox 浏览器实例。
要运行针对一个不同浏览器的测试,只需将浏览器的名称作为第二个参数提供。例如,*iexplore
使用 Internet Explorer 代替 Firefox。对 Opera 浏览器使用 *opera
,或对 Apple Safari 使用*safari
。
注意,这些浏览器必须受到运行测试的操作系统的支持。因此,如果您企图对您的操作系统上不存在的浏览器(比如运行 Linux® 的系统上的 *safari
或 *iexplore
)执行测试,您将得到一些异常,测试也会失败。
结束语
本文介绍了 Selenium 及其两个组件:Selenium IDE 和 Selenium RC。Selenium IDE 是开始为您的 web 应用程序编写测试的一种简单方法。Selenium RC 是一个可用于添加特性并自动化您的测试的组件。
从浏览器的角度看,自动化的 web 应用程序测试可能是扩大覆盖您的应用程序其他部分的现有单元测试的一种极好方法。通过自动化的测试,开发人员能够更快地获取问题反馈,测试回归也会变得更快。