测试
创建一系列良好的单元测试用例是确保应用程序在其整个生命周期内质量的重要组成部分。为了帮助开发人员进行测试工作,GWT 提供了与流行的 JUnit 单元测试框架和 Emma 代码覆盖率工具的集成。GWT 允许 JUnit 测试用例在开发模式或生产模式下运行。
注意:要了解如何将 JUnit 测试添加到示例 GWT 应用中,请参阅教程 使用 JUnit 对 GWT 应用程序进行单元测试。
为测试设计应用程序架构
本页面的大部分内容都致力于解释如何通过 GWTTestCase 类对 GWT 代码进行单元测试,最终必须为在浏览器中运行付出性能代价。但这不是你总是想做的事情。
设计应用程序以使大部分代码不知道它将在浏览器中运行,这将非常值得你的努力。你可以将以这种方式隔离的代码在运行在 JRE 中的普通 JUnit 测试用例中进行测试,从而执行速度快得多。分离关注点、依赖注入等良好的习惯将使你的 GWT 应用受益,就像它们会使任何其他应用受益一样,甚至可能比平时更多。
有关这些方面的提示,请查看为你的 GWT 应用设计最佳实践演讲(视频 或 幻灯片),该演讲于 2009 年 5 月在 Google I/O 上发表。并密切关注本网站,以了解更多同类文章。
创建测试用例
本节将描述如何为你的 GWT 项目创建和运行一组单元测试用例。要使用此功能,你必须在系统上安装 JUnit 库。
GWTTestCase 类
GWT 包含一个特殊的 GWTTestCase 基类,它提供了 JUnit 集成。在 JUnit 下运行编译的 GWTTestCase 子类将启动 HtmlUnit 浏览器,它用于在测试执行期间模拟应用程序行为。
GWTTestCase 继承自 JUnit 的 TestCase。设置 JUnit 测试用例类的典型方法是让它扩展TestCase
,然后使用 JUnit TestRunner 运行它。TestCase
使用反射来发现你的派生类中定义的测试方法。惯例是在所有测试方法的名称前添加test
前缀。
使用 webAppCreator
GWT 包含的 webAppCreator 可以为你生成一个启动测试用例,以及用于在开发模式和生产模式下进行测试的 ant 目标和 eclipse 启动配置。
例如,要创建包含测试用例的启动应用程序,该应用程序位于目录fooApp
中,其中模块名称为com.example.foo.Foo
~/Foo> webAppCreator -out fooApp
-junit /opt/eclipse/plugins/org.junit_3.8.1/junit.jar
com.example.foo.Foo
Created directory fooApp/src
Created directory fooApp/war
Created directory fooApp/war/WEB-INF
Created directory fooApp/war/WEB-INF/lib
Created directory fooApp/src/com/example/foo
Created directory fooApp/src/com/example/foo/client
Created directory fooApp/src/com/example/foo/server
Created directory fooApp/test/com/example/foo/client
Created file fooApp/src/com/example/foo/Foo.gwt.xml
Created file fooApp/war/Foo.html
Created file fooApp/war/Foo.css
Created file fooApp/war/WEB-INF/web.xml
Created file fooApp/src/com/example/foo/client/Foo.java
Created file fooApp/src/com/example/foo/client/GreetingService.java
Created file fooApp/src/com/example/foo/client/GreetingServiceAsync.java
Created file fooApp/src/com/example/foo/server/GreetingServiceImpl.java
Created file fooApp/build.xml
Created file fooApp/README.txt
Created file fooApp/test/com/example/foo/client/FooTest.java
Created file fooApp/.project
Created file fooApp/.classpath
Created file fooApp/Foo.launch
Created file fooApp/FooTest-dev.launch
Created file fooApp/FooTest-prod.launch
Created file fooApp/war/WEB-INF/lib/gwt-servlet.jar
按照生成的 fooApp/README.txt 文件中的说明操作。你有两种方法可以运行测试:使用 ant 或使用 Eclipse。有 ant 目标ant test.dev
和ant test.web
分别用于在开发模式和生产模式下运行测试。类似地,你可以按照 README.txt 文件中的说明将项目导入 Eclipse 或你喜欢的 IDE,并使用启动配置FooTest-dev
和FooTest-prod
来分别在开发模式和生产模式下使用 eclipse 运行测试。随着你不断将测试逻辑添加到骨架FooTest.java
中,你可以继续使用上述方法来运行测试。
手动创建测试用例
如果你不想使用 webAppCreator,可以按照以下说明手动创建一个测试用例套件
- 定义一个扩展 GWTTestCase 的类。确保你的测试类位于模块源路径上(例如,在你的模块的
client
子包中)。你可以通过编辑 模块 XML 文件 并添加<source>
元素来添加新的源路径。 - 如果你还没有 GWT 模块,请创建一个 模块,该模块会导致你的测试用例的源代码被包含在内。如果你正在将测试用例添加到现有的 GWT 应用程序中,你可以直接使用现有的模块。
- 实现方法 GWTTestCase.getModuleName() 以返回模块的全限定名。这是告诉 JUnit 测试用例要实例化哪个模块的粘合剂。
- 将你的测试用例类编译为字节码。你可以使用 Java 编译器直接使用 javac 或 Java IDE(如 Eclipse)。
- 运行你的测试用例。使用类
junit.textui.TestRunner
作为你的主类,并将你的测试类的完整名称作为命令行参数传递,例如com.example.foo.client.FooTest
。在运行测试用例时,确保你的类路径包含 -
- 你的项目的
src
目录
- 你的项目的
bin
目录 gwt-user.jar
库gwt-dev.jar
库junit.jar
库
- 你的项目的
客户端示例
首先,你需要一个有效的 GWT 模块来托管你的测试用例类。通常,你不需要创建新的 模块 XML 文件 - 你可以直接使用已创建的用于开发 GWT 模块的文件。但如果你还没有模块,你可以创建一个这样的模块
<module>
<!-- Module com.example.foo.Foo -->
<!-- Standard inherit. -->
<inherits name='com.google.gwt.user.User'/>
<!-- implicitly includes com.example.foo.client package -->
<!-- OPTIONAL STUFF FOLLOWS -->
<!-- It's okay for your module to declare an entry point. -->
<!-- This gets ignored when running under JUnit. -->
<entry-point class='com.example.foo.FooModule'/>
<!-- You can also test remote services during a JUnit run. -->
<servlet path='/foo' class='com.example.foo.server.FooServiceImpl'/>
</module>
提示:你不需要为每个测试用例创建单独的模块,实际上,你为创建的每个模块都要付出启动代价。在上面的示例中,com.example.foo.client
(或任何子包)中的任何测试用例都可以共享com.example.foo.Foo
模块。
假设你已经在foo
包下创建了一个小部件UpperCasingLabel
,它确保它显示的文本全部是大写字母。以下是如何测试它。
package com.example.foo.client;
import com.google.gwt.junit.client.GWTTestCase;
public class UpperCasingLabelTest extends GWTTestCase {
/**
* Specifies a module to use when running this test case. The returned
* module must include the source for this class.
*
* @see com.google.gwt.junit.client.GWTTestCase#getModuleName()
*/
@Override
public String getModuleName() {
return "com.example.foo.Foo";
}
public void testUpperCasingLabel() {
UpperCasingLabel upperCasingLabel = new UpperCasingLabel();
upperCasingLabel.setText("foo");
assertEquals("FOO", upperCasingLabel.getText());
upperCasingLabel.setText("BAR");
assertEquals("BAR", upperCasingLabel.getText());
upperCasingLabel.setText("BaZ");
assertEquals("BAZ", upperCasingLabel.getText());
}
}
现在,有几种方法可以运行测试。只需查看由 webAppCreator 生成的示例 ant 脚本或启动配置,如上一节所示。
向测试基础设施传递参数
测试基础设施中的主类是JUnitShell
。要控制测试执行方式的各个方面,你必须向此类传递参数。参数不能直接通过命令行传递,因为正常的命令行参数会直接传递给 JUnit 运行器。相反,请定义系统属性gwt.args
来向JUnitShell
传递参数。
例如,要在(旧版)开发模式下运行测试(即,在 JVM 中以 Java 运行测试),在调用 JUnit 时,将-Dgwt.args="-devMode"
声明为 JVM 参数。要获取支持选项的完整列表,请声明-Dgwt.args="-help"
(而不是运行测试,帮助信息将打印到控制台)。
在(旧版)开发模式下运行测试
使用 webAppCreator 工具时,你可以启动测试以在(旧版)开发模式或生产模式下运行。默认情况下,测试在 生产模式 下运行,因此它们会在执行之前被编译为 JavaScript。
否则,在 (旧版)开发模式 下,测试将作为普通的 Java 字节码在 JVM 中运行。虽然这使得调试它们更容易,但请注意,虽然很少见,但 Java 和 JavaScript 之间存在 一些差异,这些差异会导致你的代码在部署时产生不同的结果。
如果您决定从命令行运行 JUnit TestRunner,则需要传递参数到 JUnitShell
,才能在(遗留)开发模式下运行您的单元测试。
-Dgwt.args="-devMode"
在手动模式下运行您的测试
手动模式测试允许您在任何浏览器上手动运行单元测试。在此模式下,JUnitShell
主类照常在指定 GWT 模块上运行,但它不会立即运行测试,而是打印出一个 URL 并等待浏览器连接。您可以手动将此 URL 剪切粘贴到您选择的浏览器中,单元测试将在该浏览器中运行。
例如,如果您想在单个浏览器中运行测试,您将使用以下参数
-runStyle Manual:1
然后,GWT 会显示类似于以下内容的控制台消息
Please navigate your browser to this URL:
http://172.29.212.75:58339/com.google.gwt.user.User.JUnit/junit.html?gwt.codesvr=172.29.212.75:42899
将您的浏览器指向指定的 URL,测试将运行。第一次运行测试时,GWT 开发人员插件可能会提示您接受连接。
手动模式测试目标不是由 webAppCreator 工具生成的,但您可以通过将 build.xml 文件中的 test.prod
ant 目标复制到 test.manual
并在 -Dgwt.args
部分添加 -runStyle Manual:1
来轻松创建一个。手动模式也可以用于远程浏览器测试。
在远程系统上运行您的测试
由于不同的浏览器通常会以意想不到的方式运行,因此开发人员在他们计划支持的所有浏览器上测试他们的应用程序非常重要。GWT 通过使您能够在远程系统上运行测试来简化远程浏览器测试,如远程浏览器测试页面中所述。
自动化您的测试用例
在开发大型项目时,一个良好的做法是将运行您的测试用例集成到您的常规构建过程中。当您手动构建时,例如从命令行使用 ant
或使用您的桌面 IDE,这与简单地将 JUnit 的调用添加到您的常规构建过程一样简单。如前所述,当您运行 GWTTestCase
测试时,HtmlUnit 浏览器会运行测试。但是,正如前面解释的那样,所有测试可能无法在 HtmlUnit 上成功运行。GWT 提供了远程测试解决方案,允许您使用 Selenium 服务器运行测试。此外,请考虑将您的测试组织到GWTTestSuite 类中,以从您的单元测试获得最佳性能。
服务器端测试
上面描述的测试旨在帮助测试客户端代码。测试用例包装器 GWTTestCase
将启动一个开发模式会话或 Web 浏览器来测试生成的 JavaScript。另一方面,服务器端代码在 JVM 中作为本机 Java 运行,而不会被转换为 JavaScript,因此不需要使用 GWTTestCase
作为测试的基类来运行服务器端代码的测试。相反,在为您的应用程序的服务器端代码编写测试时,直接使用 JUnit 的 TestCase
和其他相关类。也就是说,您可能希望同时拥有 GWTTestCase 和 TestCase 对将在客户端和服务器端使用的代码的覆盖范围。
异步测试
GWT 的JUnit 集成提供了对测试无法在直线代码中执行的功能的特殊支持。例如,您可能希望对服务器进行RPC 调用,然后验证响应。但是,在正常的 JUnit 测试运行中,测试会在测试方法将控制权返回给调用者后立即停止,并且 GWT 不支持多线程或阻塞。为了支持此用例,GWTTestCase 扩展了 TestCase
API。两个关键方法是GWTTestCase.delayTestFinish(int) 和GWTTestCase.finishTest()。在测试方法执行期间调用 delayTestFinish()
会将该测试置于异步模式,这意味着测试不会在测试方法将控制权返回给调用者时结束。相反,一个延迟周期开始,持续时间为在对 delayTestFinish()
的调用中指定的时间量。在延迟期间,测试系统将等待以下三种情况之一发生
- 如果在延迟周期到期之前调用
finishTest()
,测试将成功。 - 如果在延迟周期内有任何异常从事件处理程序中逸出,测试将因抛出的异常而出错。
- 如果延迟周期到期并且上述情况都没有发生,测试将因TimeoutException 而出错。
正常的使用模式是在测试方法中设置一个事件,并使用比事件预期花费的时间长得多的超时调用 delayTestFinish()
。事件处理程序验证事件,然后调用 finishTest()
。
例子
public void testTimer() {
// Setup an asynchronous event handler.
Timer timer = new Timer() {
public void run() {
// do some validation logic
// tell the test system the test is now done
finishTest();
}
};
// Set a delay period significantly longer than the
// event is expected to take.
delayTestFinish(500);
// Schedule the event and return control to the test system.
timer.schedule(100);
}
建议的模式是每个测试方法测试一个异步事件。如果您需要在同一个方法中测试多个事件,这里有两种技术
- 将事件“链接”在一起。在测试方法执行期间触发第一个事件;当该事件触发时,再次使用新的超时调用
delayTestFinish()
并触发下一个事件。当最后一个事件触发时,照常调用finishTest()
。 - 设置一个计数器,包含要等待的事件数。当每个事件到来时,递减计数器。当计数器达到
0.
时,调用finishTest()
。
将 TestCase 类组合到 TestSuite 中
由于必须启动开发模式 shell 和 servlet 或者编译代码,GWTTestSuite 机制具有开销。套件中的每个测试模块也都有开销。
理想情况下,您应该将测试尽可能少地分组到模块中,并且应该避免在特定模块中运行的测试超过一个套件。(如果测试返回来自getModuleName() 的相同值,则测试位于同一个模块中。)
GWTTestSuite 类重新排序测试用例,以便所有共享模块的用例都按顺序运行。
如果您已经定义了单独的 JUnitTestCases 或GWTTestCases,创建套件很简单。以下是一个示例
public class MapsTestSuite extends GWTTestSuite {
public static Test suite() {
TestSuite suite = new TestSuite("Test for a Maps Application");
suite.addTestSuite(MapTest.class);
suite.addTestSuite(EventTest.class);
suite.addTestSuite(CopyTest.class);
return suite;
}
}
现在,三个测试用例 MapTest
、EventTest
和 CopyTest
都可以在 JUnitShell 的同一个实例中运行。
java -Xmx256M -cp "./src:./test:./bin:./junit.jar:/gwt/gwt-user.jar:/gwt/gwt-dev.jar:/gwt/gwt-maps.jar" junit.textui.TestRunner com.example.MapsTestSuite
设置和拆卸使用 GWT 代码的 JUnit 测试用例
在 JUnitTestCase 中使用测试方法时,测试创建的任何对象和留下的引用都将保持活动状态。这可能会干扰未来的测试方法。您可以覆盖两个新方法,以便为每个测试方法准备和/或清理。
- gwtSetUp() 在测试用例中的每个测试方法之前运行。
- gwtTearDown() 在测试用例中的每个测试方法之后运行。
以下示例展示了如何使用gwtSetUp() 在下次测试运行之前防御性地清理 DOM。它跳过 <iframe>
和 <script>
标签,以防止意外删除 GWT 测试基础结构。
import com.google.gwt.junit.client.GWTTestCase;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element;
private static native String getNodeName(Element elem) /*-{
return (elem.nodeName || "").toLowerCase();
}-*/;
/**
* Removes all elements in the body, except scripts and iframes.
*/
public void gwtSetUp () {
Element bodyElem = RootPanel.getBodyElement();
List<Element> toRemove = new ArrayList<Element>();
for (int i = 0, n = DOM.getChildCount(bodyElem); i < n; ++i) {
Element elem = DOM.getChild(bodyElem, i);
String nodeName = getNodeName(elem);
if (!"script".equals(nodeName) && !"iframe".equals(nodeName)) {
toRemove.add(elem);
}
}
for (int i = 0, n = toRemove.size(); i < n; ++i) {
DOM.removeChild(bodyElem, toRemove.get(i));
}
}
在 Eclipse 中运行测试
webAppCreator 工具提供了一种简单的方法来生成示例启动配置,这些配置可用于在 Eclipse 中运行开发模式和生产模式测试。您可以通过复制它并相应地替换项目名称来生成其他启动配置。
或者,也可以直接生成启动配置。通过右键单击扩展 GWTTestCase
的测试文件并选择 以…方式运行
> JUnit 测试
来创建一个正常的 JUnit 运行配置。尽管第一次运行会失败,但会生成一个新的 JUnit 运行配置。通过将项目的 src
和 test
目录添加到类路径来修改运行配置,如下所示
- 单击
类路径
选项卡 - 选择
用户条目
- 单击
高级
按钮 - 选择
添加文件夹
单选按钮 - 添加您的
src
和test
目录
启动运行配置以查看在开发模式下运行的测试。
要在生产模式下运行测试,请复制开发模式启动配置并传递 VM 参数(通过单击 参数
选项卡并添加到 VM 参数文本区域)-Dgwt.args="-prod"