Testing
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against GWT 2.13.1 (2026-06-19), gwt-maven-plugin 1.3.0, JDK 21 with -sourceLevel 17, the
HtmlUnit 2.55.0 and Jetty 9.4.58 that the GWT BOM manages for the test runner, and the Jakarta Servlet 6 stack of the
modular-webapp archetype, checked on 2026-10-09. It is about build and tooling (how the tests are compiled and
run), about client code that is tested after compilation to JavaScript, and about shared and server code that
is tested as plain Java. Every command and every output block below was produced in a private copy of the Bookshelf
scratch application of this section: a bookshelf-shared, a bookshelf-client and a bookshelf-server module, 19 JUnit
tests and 2 Playwright tests.
A GWT application has three kinds of code and each needs a different kind of test. Most of the code is plain Java that
never touches the browser, and it should be tested on the JVM in milliseconds. The code that calls GWT.create, uses
JSNI or JsInterop, or builds widgets only works after the GWT compiler has processed it, and GWTTestCase exists for
that. The whole application, deployed and driven through a browser, is the job of an end-to-end tool.
The testing pyramid for a GWT application
Playwright drives Chrome against the deployed app
2 tests, about 1 second, needs a running server"] gwt["GWTTestCase
compiled client code in HtmlUnit, started by gwt:test
8 tests, 7 to 13 seconds, one GWT compile per test module"] jvm["Plain JUnit on the JVM
presenters, server classes, shared types
11 tests, milliseconds, no browser and no GWT compile"] e2e --- gwt gwt --- jvm
The numbers are the Bookshelf ones. The point is the ratio: keep the logic in classes that the bottom layer can test, keep
the GWTTestCase layer for what only works inside the compiler, and keep the end-to-end layer for a few journeys.
| Layer | Runs | Good for | Cannot check |
|---|---|---|---|
Plain JUnit (JVM) |
Compiled Java bytecode on the JVM, Surefire style |
Presenters and activities with fake views, server classes called directly, shared types, anything that never calls |
Anything that needs the compiler: deferred binding ( |
|
The module is compiled to JavaScript for the test, then run in an emulated Firefox inside the same JVM |
Generated code (i18n messages, UiBinder, ClientBundle, RPC proxies; see Internationalization, UiBinder), widgets and DOM behaviour, |
Layout (HtmlUnit has none), real browser differences, the production application as deployed |
Playwright (or Selenium, Cypress) |
A real browser against the deployed application |
The whole stack: host page, linker output, servlet, session, navigation |
Fine-grained logic. It is slow and needs a server |
The Bookshelf test layout
The module bookshelf-client is compiled by the GWT compiler from the packages named by its <source> elements (client
and shared). Everything in those packages is translated to JavaScript when a test module is compiled, including test
classes that live there. That decides where each kind of test goes:
bookshelf-client/src/test/java/com/example/bookshelf/
client/BookshelfGwtTest.java GWTTestCase (inside the module source path, must be)
client/CatalogAsyncGwtTest.java GWTTestCase (asynchronous tests)
client/SpanishMessagesGwtTest.java GWTTestCase (a second test module)
client/rpc/PingService.java test-only RPC interface and its Async twin (client code)
testserver/PingServiceImpl.java test-only servlet (server code: outside the source path)
suite/AllGwtTests.java GWTTestSuite (JUnit-only class: outside the source path)
unit/CatalogPresenterTest.java plain JUnit (outside the source path)
bookshelf-client/src/test/resources/com/example/bookshelf/
AppTest.gwt.xml test module for the GWTTestCase classes
AppEsTest.gwt.xml second test module, locale pinned to Spanish
bookshelf-server/src/test/java/.../server/ CatalogRepositoryTest, CatalogServiceImplTest
bookshelf-shared/src/test/java/.../shared/ SharedTypesTest
GWTTestCase classes must be on the module source path, because the compiler has to see their source. Plain JUnit
classes, suites and servlets must be outside it. Section Pitfalls shows what happens when they are not. The rule is the
same one the page about Activities and Places used for its navigation tests.
GWTTestCase
com.google.gwt.junit.client.GWTTestCase extends JUnit’s junit.framework.TestCase, so it is a JUnit 3 style class:
the test methods are the public void methods whose names start with test, and annotations such as @Test are not
used. It runs under the JUnit 4 provider that gwt:test selects. Under the same fully qualified name there is a second,
translatable class (in the translatable source tree), which is what actually runs in the browser. The JVM-side class only
starts the machinery and carries the result back.
A first test
A GWTTestCase subclass has one obligation: getModuleName() returns the name of a module whose source path contains the
test class. The Bookshelf test module inherits the application module and pins what the permutations would otherwise
multiply:
<?xml version="1.0" encoding="UTF-8"?>
<module rename-to="apptest">
<inherits name="com.example.bookshelf.App" />
<!-- One locale and one user agent keep the test compile to a single permutation -->
<set-property name="locale" value="en" />
<set-property name="user.agent" value="gecko1_8" />
<!-- Mapped on the embedded Jetty 9 test server, relative to the module base URL -->
<servlet path="/ping" class="com.example.bookshelf.testserver.PingServiceImpl" />
</module>
public class BookshelfGwtTest extends GWTTestCase {
@Override
public String getModuleName() { return "com.example.bookshelf.AppTest"; }
public void testPluralMessages() {
assertEquals("en", LocaleInfo.getCurrentLocale().getLocaleName());
AppMessages msg = GWT.create(AppMessages.class); // deferred binding runs in the test
assertEquals("No copies", msg.copies(0));
assertEquals("1 copy", msg.copies(1));
assertEquals("3 copies", msg.copies(3));
}
public void testPlaceTokens() {
AppPlaceHistoryMapper mapper = GWT.create(AppPlaceHistoryMapper.class);
assertEquals("book:7", mapper.getToken(new BookPlace(7)));
assertEquals(7L, ((BookPlace) mapper.getPlace("book:7")).getId());
}
}
Run on its own with the plugin goal (the -Dtest filter picks one class; test-compile is needed when the sources
changed, because gwt:test itself does not compile):
export JAVA_HOME=/path/to/jdk-21
mvn -B test-compile gwt:test -pl bookshelf-client -Dtest=BookshelfGwtTest
[INFO] --- gwt:1.3.0:test (default-cli) @ bookshelf-client ---
[INFO] Using auto detected provider org.apache.maven.surefire.junit4.JUnit4Provider
[INFO] T E S T S
[INFO] Running com.example.bookshelf.client.BookshelfGwtTest
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 5.888 s -- in com.example.bookshelf.client.BookshelfGwtTest
Source: gwt-maven-plugin — gwt:test
The 6 seconds are almost all the GWT compile of the test module (one permutation); the three test methods take milliseconds.
What happens during a run
com.google.gwt.junit.JUnitShell is a subclass of the Dev Mode launcher. It builds a synthetic module named
<module>.JUnit (here com.example.bookshelf.AppTest.JUnit) that inherits your test module and adds the JUnit runtime,
compiles it, starts an embedded Jetty and launches the run style, which decides which browser loads the page. The
page’s GWTRunner then pulls test blocks from the server over GWT-RPC, runs them and reports back. The JUnit module the synthetic module inherits turns on jre.debugMode and emulated stack traces, which is why failure
traces in the reports show Java class and method names. The page-plus-server design is also why a test can
fail with TimeoutException: The browser did not contact the server within 60000ms. (nothing loaded the page) and why the
test module can declare <servlet> elements.
Module names and pure Java mode
-
One module name per test class is the key to speed: every distinct module costs one GWT compile. Put tests that need the same configuration in the same module and the same suite (see GWTTestSuite and one module per test class).
-
getModuleName()may returnnull. The test then runs as a plain JUnit test on the JVM ("pure Java mode"),isPureJava()returnstrue, andgwtSetUp/gwtTearDownstill run.setForcePureJava(true)has the same effect. -
The module needs the permutations pinned, or the test fails with an
AssertionError(section Pitfalls). Two ways: a test module with<set-property>as above, or arguments on the command line (next sections).
gwtSetUp and gwtTearDown
setUp() and tearDown() are final in GWTTestCase. Override gwtSetUp() and gwtTearDown() instead; both are
protected and run once per test method, inside the browser. The page of RootPanel is shared by all test methods of a
module run, so a test that attaches widgets should clean up:
@Override
protected void gwtTearDown() {
RootPanel.get().clear();
}
Asynchronous tests
A test method that returns ends the test, and GWT code cannot block. A test of a Timer (see Delayed Logic & Scheduler), an RPC call or an event
therefore calls delayTestFinish(int) with a timeout, returns, and calls finishTest() from the callback. Three things can
happen during the delay: finishTest() is called (success), an exception escapes from an event handler (error with that
exception), or the delay expires (error with a TimeoutException). delayTestFinish and finishTest are protected
final methods of GWTTestCase; calling delayTestFinish again resets the timeout, which is how events are chained.
The Bookshelf tests one Timer and one real GWT-RPC round trip against a test servlet. The servlet extends the
javax flavour of RemoteServiceServlet because the embedded test server is Jetty 9 (javax.servlet); the Jakarta
flavour is for the application server, not for the module <servlet> element.
@RemoteServiceRelativePath("ping")
public interface PingService extends RemoteService {
String ping(String name);
}
public interface PingServiceAsync {
void ping(String name, AsyncCallback<String> callback);
}
// package com.example.bookshelf.testserver: outside the module source path
public class PingServiceImpl extends com.google.gwt.user.server.rpc.RemoteServiceServlet
implements PingService {
@Override
public String ping(String name) { return "pong " + name; }
}
public void testTimer() {
Timer timer = new Timer() {
@Override
public void run() { finishTest(); }
};
delayTestFinish(2000); // much longer than the 100 ms the timer needs
timer.schedule(100);
}
public void testRpcRoundTrip() {
PingServiceAsync service = GWT.create(PingService.class);
delayTestFinish(10000);
service.ping("Bookshelf", new AsyncCallback<String>() {
@Override
public void onSuccess(String result) {
assertEquals("pong Bookshelf", result);
finishTest();
}
@Override
public void onFailure(Throwable caught) { fail(caught.toString()); }
});
}
Both passed. Forcing the three failure modes in a scratch test (delayTestFinish(300) and nothing else; finishTest()
alone; an exception thrown from a Timer) printed these messages, in that order:
com.google.gwt.junit.client.TimeoutException: A timeout expired after 300ms elapsed.
java.lang.IllegalStateException: This test is not in asynchronous mode; call delayTestFinish() first
java.lang.IllegalStateException: boom in timer
Source: GWT source — GWTTestCase.java
Test one asynchronous event per method. The test servlet must be reachable at the URL the proxy computes: the proxy asks
for <module base URL>ping, and the module XML maps /ping. The application’s XsrfProtectedService endpoints need a
token; test those on the JVM or end to end (see the page about
XSRF and server security) rather than through this shortcut.
Skipping and varying tests
-
@DoNotRunWith(Platform.HtmlUnitLayout)(classcom.google.gwt.junit.DoNotRunWith, enumcom.google.gwt.junit.Platform) skips a test class or method on run styles that cannot run it. The enum has exactlyDevel,HtmlUnitBug,HtmlUnitLayout,HtmlUnitUnknownandProd. The HtmlUnit run style bans the threeHtmlUnit*values, so tests that need real layout, or that hit an HtmlUnit bug, are skipped there and still run in a real browser. The official page still showsPlatform.HtmlUnit, which does not exist in 2.13.1. -
@WithProperties(@Property(name = "locale", value = "es"))(classcom.google.gwt.junit.client.WithProperties) binds a property for one test method. The runner gives such a method its own synthetic module, whose name carries the property (...JUnit.locale_es), so it is compiled separately.
@DoNotRunWith(Platform.HtmlUnitLayout)
public void testLabelHasHeight() {
Label label = new Label("Effective Java");
RootPanel.get().add(label);
assertTrue(label.getOffsetHeight() > 0); // HtmlUnit has no layout engine
}
@WithProperties(@Property(name = "locale", value = "es"))
public void testSpanishFromAnnotation() {
AppMessages msg = GWT.create(AppMessages.class);
assertEquals("Sin ejemplares", msg.copies(0));
}
Without the @DoNotRunWith line the first test fails under HtmlUnit with
testLabelHasHeight ... expected: <true>, actual: <false>; with it the test is skipped there (the report counts it as run).
Event simulation has the same limits (see the page about events and handlers).
GWTTestSuite and one module per test class
A test class is run by JUnit one at a time, and the shell keeps the synthetic module of the current test class. When two
GWTTestCase classes share a module name and run back to back, the second one reuses the page that was compiled for the
first one, which does not contain it. Run the two Bookshelf classes consecutively:
mvn -B test-compile gwt:test -pl bookshelf-client -Dtest=BookshelfGwtTest,CatalogAsyncGwtTest
[INFO] Running com.example.bookshelf.client.BookshelfGwtTest
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 10.39 s -- in com.example.bookshelf.client.BookshelfGwtTest
[INFO] Running com.example.bookshelf.client.CatalogAsyncGwtTest
[ERROR] Tests run: 4, Failures: 0, Errors: 2, Skipped: 0, Time elapsed: 2.230 s <<< FAILURE! -- in com.example.bookshelf.client.CatalogAsyncGwtTest
java.lang.RuntimeException: com.example.bookshelf.AppTest.JUnit:com.example.bookshelf.client.CatalogAsyncGwtTest.testTimer: could not instantiate the requested class
Caused by: com.google.gwt.core.client.JavaScriptException: (TypeError) : Cannot call method "new" of undefined
Two of the four tests failed: the two that need no extra module. The test with @WithProperties got its own module and
passed, and the layout test was skipped. The failure is order dependent: in the plain mvn verify run, where the
order in which Surefire ran them put SpanishMessagesGwtTest (another module) between the two, all of them passed. A suite that is
green today turns red when someone adds a class. The official guide gives the remedy: group the classes in a
com.google.gwt.junit.tools.GWTTestSuite, which reorders the tests so that all cases of one module run together, once.
The guide also advises few modules and not running one module’s tests from more than one suite, because each module and
each suite pays the compile again.
package com.example.bookshelf.suite;
public class AllGwtTests extends GWTTestSuite {
public static Test suite() {
TestSuite suite = new TestSuite("Bookshelf GWT tests");
suite.addTestSuite(BookshelfGwtTest.class);
suite.addTestSuite(CatalogAsyncGwtTest.class);
suite.addTestSuite(SpanishMessagesGwtTest.class); // another module: grouped, compiled once each
return suite;
}
}
The plugin must run the suite and not the member classes, otherwise they run twice (and the first problem returns). That is
what the includes of the client module do:
<plugin>
<groupId>net.ltgt.gwt.maven</groupId>
<artifactId>gwt-maven-plugin</artifactId>
<configuration>
<moduleName>com.example.bookshelf.App</moduleName>
<includes>
<include>**/unit/*Test.java</include>
<include>**/suite/AllGwtTests.java</include>
</includes>
</configuration>
</plugin>
[INFO] Running com.example.bookshelf.unit.CatalogPresenterTest
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.029 s -- in com.example.bookshelf.unit.CatalogPresenterTest
[INFO] Running com.example.bookshelf.suite.AllGwtTests
[INFO] Tests run: 8, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 11.43 s -- in com.example.bookshelf.suite.AllGwtTests
[INFO] Tests run: 10, Failures: 0, Errors: 0, Skipped: 0
Two alternatives were tried and rejected. -Dgwt.args="-batch class" is an undocumented option (isUndocumented() in
JUnitShell), and with it the same two classes ended in TimeoutException: The browser did not contact the server within
60000ms.. Keeping one class per module works, but costs a compile each, and the classes still need module names that
differ.
Running the tests with gwt:test
The goal and its parameters
The gwt-app and gwt-lib packagings run gwt:test in the test phase instead of Surefire (see
Build Tools: Maven and Gradle for the lifecycle and the full parameter
table). It is built on Surefire, so -Dtest, includes, excludes, skipTests, argLine, forkCount and the reports
behave as they do there. The parameters that matter here:
| Parameter | Property | Effect |
|---|---|---|
|
none (a list in the POM) |
Arguments passed to |
|
none |
Compiler arguments, prepended to |
|
|
Default |
|
|
Compiler settings for the test module |
|
|
Documented as the ports of the embedded server and the code server (automatic by default) |
|
none |
Output under |
Source: gwt-maven-plugin — gwt:test
In this setup -Dgwt.port=9954 did not change the port of a Manual run (the URL it printed still had a random port),
while -port 9954 inside -Dgwt.args (an option of JUnitShell, default 8888 in the shell’s own help) printed
http://localhost:9954/com.example.bookshelf.AppTest.JUnit/junit.html. Pass the port that way when a fixed one is needed.
Use mvn verify (or install) and not mvn test for a client module that takes the shared module’s sources classifier
JAR, because that JAR is only produced at package. And set includes: with none, the goal printed its header and nothing
else (BUILD SUCCESS, no T E S T S, no tests) in this setup.
Passing arguments with -Dgwt.args
The JUnit runner owns the command-line arguments, so options for the GWT test infrastructure travel in the system property
gwt.args; Maven passes -Dgwt.args=... on to the forked test JVM. -Dgwt.args=-help prints the options of
JUnitShell (in the 2.13.1 run the usage was printed and the tests then failed with JUnitFatalLaunchException: Error
processing shell arguments, so use it once and do not expect a normal run). The options, as printed:
| Option | Meaning |
|---|---|
|
Which browser launcher to use (below). The default is |
|
Run the tests as Java bytecode in the JVM. Default off: tests run as compiled JavaScript. The aliases |
|
Fail compilation when any input file has an error. |
|
Pin a GWT property for the whole run |
|
Limit the user agents (permutations), for example |
|
Timeout of one test method (the shell’s default is 5) |
|
Time allowed for the browser to contact the server (the shell’s default is 1) |
|
EXPERIMENTAL: attempts per test method |
|
Show the log window and the browser windows (alias |
|
The compiler options of the same names, applied to the test module |
|
Ports of the embedded web server and of the code server |
-setProperty makes a dedicated test module unnecessary. A test class whose getModuleName() is the application module
itself (six permutations: three locales times two user agents would otherwise trip the assertion) passed with
mvn -B test-compile gwt:test -pl bookshelf-client -Dtest=TmpGwtTest \
'-Dgwt.args=-setProperty locale=en -setProperty user.agent=gecko1_8'
(TmpGwtTest was a throwaway class with one GWT.create(AppMessages.class) assertion.) The same module without the
arguments failed with the AssertionError of section Pitfalls, and with -DenableAssertions=false it passed too.
Prefer the pinned module for committed tests: it also keeps the IDE runs identical.
Run styles
-runStyle names a class in the com.google.gwt.junit package (RunStyle plus the name you pass) or a fully qualified
RunStyle subclass, optionally followed by a colon and an argument. The 2.13.1 source tree contains these run styles:
| Run style | Argument | Verified result |
|---|---|---|
|
Comma separated browsers: |
|
|
Number of browsers to wait for, for example |
Printed |
|
Comma separated executables, launched with the test URL as the only argument |
|
|
|
Not run. The constructor logs that it is deprecated for removal, with the pointer to issue #10222 |
Source: GWT source — RunStyleHtmlUnit.java, GWT source — RunStyleManual.java, GWT source — RunStyleExternalBrowser.java
-
HtmlUnit is the default and the only one that needs nothing installed. The default emulation reported itself as Firefox (
Firefox/94.0in the user agent); the official page still says Firefox 38 andFF38,IE11, names that no longer exist. The accepted names are the nicknames of HtmlUnit 2.55.0’s browser versions.HtmlUnit:Safarifails withRunStyleHtmlUnit: Unknown browser name Safari, expected browser name: one of [FF, Chrome, IE, Edge], although the 2.10 release notes list Safari.ChromeandEdgeselect thesafariuser-agent permutation of the test module,FFandIEthegecko1_8one, so a test module that pinsuser.agentto one value only exercises that browser’s code. -
Manual is the way to run the tests in a browser that you drive yourself, including a browser on another machine. The run waits for a client (the
-testBeginTimeoutapplies) and prints how many clients connected. -
ExternalBrowser is not mentioned in the official guide (the class is package private) but works as a CI run style: a Chrome for Testing or a headless-shell executable runs the same compiled tests in real Blink. It prints nothing about the browser; look for
Tests run:. -
Selenium is deprecated for removal as of 2.13 (issue #10222, closed) and its source can be copied into your own project. RemoteWeb, with its
BrowserManagerServerand thermi://argument that the Remote Testing page documents, is not in the 2.13.1 source tree at all, and the page itself says it was due for removal in GWT 2.7. Treat that whole page, and its Firefox profile with the GWT Developer Plugin, as historical.
Production mode, development mode and strictness
The official Testing page says that tests run in development mode by default and that -prod selects production mode. In 2.13.1
the shell’s developmentMode field is initialised to false, -devMode is the flag that turns it on, and
-prod and -web are its negated aliases. The default is production mode: the module is compiled to JavaScript and
that JavaScript is what runs. Classic Dev Mode has been deprecated since 2.8.0, but -devMode still exists:
mvn -B gwt:test -pl bookshelf-client -Dtest=BookshelfGwtTest '-Dgwt.args=-devMode' \
'-DargLine=--add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED'
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 3.020 s -- in com.example.bookshelf.client.BookshelfGwtTest
On JDK 21 it needs both --add-opens. Without any, the run took 64 seconds and ended with
InaccessibleObjectException: Unable to make protected void java.lang.Object.finalize() throws java.lang.Throwable accessible: module java.base does not "opens java.lang" to unnamed module;
with java.lang alone the DataGrid test failed on java.util.AbstractCollection.finishToArray. It is faster (3 s
against 6 to 8 s, no JavaScript compile) and the code is debuggable in the IDE, but it tests Java semantics, not the
JavaScript that ships, so use it for debugging and keep production mode in CI.
-strict (the alias of -failOnError) makes the test compile fail on any source file with an error. By default the
compiler ignores units with errors in its first pass and only logs Ignored N unit(s) with compilation errors in first pass. Compile with -strict or with -logLevel set to ... at INFO level. That default hides test classes that are on the
module source path and use non-GWT APIs. With -strict they stop the run. GWT’s own milestone 2.14 has an open item,
issue #10038, to enable -strict for user tests; in 2.13.1 you opt in:
mvn -B gwt:test -pl bookshelf-client -Dgwt.args=-strict
After moving the plain JUnit tests and the suite out of the module source path, this run passed (10 tests). Before, with
CatalogPresenterTest in the client package, it failed in the GWT compile, not in the test:
Line 55: No source code is available for type org.junit.Assert; did you forget to inherit a required module?. Add
-strict to a CI job and the rule "tests that are not GWTTestCase live outside the source path" enforces itself.
Plain JUnit tests for presenters and other client logic
The cheapest test of GWT client code is no GWT test. The testing methodologies article puts it plainly: standard JUnit
test cases run orders of magnitude faster than GWTTestCase, and a presenter that talks to its view through an interface
needs no widget at all. The conditions are the ones from the Model-View-Presenter pattern (see
Activities and Places):
-
the presenter receives the service interface and the view interface through its constructor,
-
it never calls
GWT.create,new Label()or any JSNI/JsInterop method, so the JVM never needs the compiler, -
the views are interfaces, so a fake or a mock stands in.
The Bookshelf CatalogPresenter is written that way (its class comment says "Plain Java: no GWT.create()"):
package com.example.bookshelf.unit; // outside the module source path
public class CatalogPresenterTest {
static class FakeService implements CatalogServiceAsync {
boolean fail;
@Override
public void getBooks(int start, int length, AsyncCallback<BookPage> cb) {
if (fail) {
cb.onFailure(new RuntimeException("boom"));
} else {
cb.onSuccess(new BookPage(List.of(new Book(1, "T", "A", 2000, 1)), 42));
}
}
// getBook and saveBook are empty
}
static class FakeDisplay implements CatalogDisplay {
int total = -1;
int start = -1;
String error;
@Override
public void showRows(int start, List<Book> rows, int total) { this.start = start; this.total = total; }
@Override
public void showError(String message) { error = message; }
}
@Test
public void loadPushesRowsToTheDisplay() {
FakeDisplay d = new FakeDisplay();
new CatalogPresenter(new FakeService(), d).load(10, 10);
assertEquals(10, d.start);
assertEquals(42, d.total);
assertNull(d.error);
}
@Test
public void failureIsReported() {
FakeService s = new FakeService();
s.fail = true;
FakeDisplay d = new FakeDisplay();
new CatalogPresenter(s, d).load(0, 10);
assertEquals("boom", d.error);
}
}
Asynchronous service calls need no delayTestFinish here: the fake calls the callback synchronously, and a fake that
holds the answer back is how the race-condition tests of the page about sorting cell widgets work (see
Cell Widgets).
When the code does call GWT.create
On the JVM, GWT.create throws, which is the useful signal that the class belongs in a GWTTestCase:
java.lang.UnsupportedOperationException: ERROR: GWT.create() is only usable in client code! It cannot be called, for example, from server code. If you are running a unit test, check that your test case extends GWTTestCase and that GWT.create() is not called from within an initializer or constructor.
com.google.gwt.junit.GWTMockUtilities.disarm() swaps in a bridge that makes GWT.create return null, and restore()
undoes it. It exists so that a test can mock the classes that GWT.create would build. The scratch test that called
disarm(), asserted assertNull(GWT.create(AppMessages.class)) and called restore() in a finally block passed. Widgets
are not usable that way: a new Label() runs JSNI that has no JVM implementation.
Mocking libraries work for presenters as usual (Mockito 5.24.0 is the current release on Maven Central at the time of
writing). GwtMockito (com.google.gwt.gwtmockito:gwtmockito) was the library that stubbed GWT.create and widgets
for plain JUnit tests. Its last release on Central is 1.1.9 (2019-10-13) and the GitHub repository google/gwtmockito is
archived (last push 2024-02-06), so do not choose it for a new project; design the views as interfaces instead.
Server and shared code on the JVM
Server code runs as ordinary Java and needs no GWT infrastructure, which the official page states directly: use
JUnit’s TestCase and related classes for server-side code and keep GWTTestCase for code that must run in a browser.
A GWT-RPC servlet is a plain object. CatalogServiceImpl extends XsrfProtectedServiceServlet (the ...rpc.jakarta
package of gwt-servlet-jakarta) can be instantiated and called without a container, because the business methods
do not touch the request:
public class CatalogServiceImplTest {
private final CatalogServiceImpl service = new CatalogServiceImpl();
@Test
public void pageLengthIsCappedAt100() {
assertEquals(25, service.getBooks(0, 10_000).getBooks().size());
}
@Test
public void unknownBookIsReportedWithTheSharedException() {
assertThrows(BookNotFoundException.class, () -> service.getBook(999));
}
@Test
public void blankTitleIsRejectedOnTheServer() {
Book book = new Book(1, " ", "A", 2000, 1);
assertThrows(IllegalArgumentException.class, () -> service.saveBook(book));
}
}
The last test checks a security rule: the title validation is on the server, whatever the client form does (see the page
about GWT-RPC). Tests of what the servlet adds around the methods (the XSRF token check, the
serialization policy, the HTTP status of an exception) need a container or a mock request; the XSRF behaviour is exercised
on the page about XSRF and server security. Remember that on JDK 17 and
later a call that serialises a server-side exception needs --add-opens java.base/java.lang=ALL-UNNAMED in the servlet
container’s JVM (upstream issue #9793); a GWTTestCase run does not hit it.
Shared code is the code both sides compile, so test it twice only where the two compilers can disagree. Types that have no GWT dependency are tested on the JVM like any class. The Bookshelf test also guards the RPC rule that every serialisable type has a default constructor (it may be private):
@Test
public void rpcTypesHaveADefaultConstructor() throws Exception {
for (Class<? extends Serializable> type :
List.of(Book.class, BookPage.class, BookNotFoundException.class)) {
assertNotNull(type.getName(), type.getDeclaredConstructor());
}
}
Where the behaviour depends on the JRE emulation (rounding, String.format, BigDecimal), a JVM test passes and the
browser disagrees; that code also needs a GWTTestCase. The page about
JRE emulation and shared code lists what differs.
Code coverage
The official Coverage page is about EMMA, an Eclipse plug-in (EclEmma) and a patched emma.jar from the GWT download page,
with ant targets and a junit-3.8.1.jar. None of that applies to a Maven build on JDK 21. The modern equivalent is
JaCoCo, which attaches a Java agent to the forked test JVM. gwt:test honours the argLine of Surefire, and JaCoCo’s
prepare-agent sets the property of that name, so the combination works:
<properties>
<!-- JaCoCo fills this in. Empty by default, or @{argLine} below is not replaced -->
<argLine></argLine>
</properties>
<plugin>
<groupId>net.ltgt.gwt.maven</groupId>
<artifactId>gwt-maven-plugin</artifactId>
<configuration>
<argLine>@{argLine}</argLine>
</configuration>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.15</version>
<executions>
<execution><goals><goal>prepare-agent</goal></goals></execution>
<execution><id>report</id><phase>verify</phase><goals><goal>report</goal></goals></execution>
</executions>
</plugin>
The scratch application ran with the same goals given on the command line (org.jacoco:jacoco-maven-plugin:0.8.15 is the
latest release on Central, published 2026-06-05). The instruction coverage per module, from the generated jacoco.csv:
| Module | Instructions covered | Observation |
|---|---|---|
|
52 of 101 |
The three plain JUnit tests |
|
287 of 469 |
|
|
47 of 836 |
Only |
The eight GWTTestCase tests exercised CatalogView, the places, the message bundle and the data grid, and JaCoCo
counted none of it. In production mode the code runs as JavaScript, so there is no bytecode to measure. In
-devMode the bytecode runs, but it is loaded by GWT’s own class loader, which the agent does not instrument; the same
run reported 0 for CatalogView and the places, and added JSNI errors such as
Member 'stringTable' in JSNI reference '@com.google.gwt.user.client.rpc.impl.ClientSerializationStreamReader::stringTable' could not be found; expect subsequent failures
to the log (the tests still passed). The practical rule is: coverage of the client comes from the plain JUnit layer.
That is one more reason to keep logic out of the views, and the number for the client module must be read as "coverage of
the presenters", not of the module. The GWTTestCase layer is judged by what it asserts, not by a percentage.
End-to-end tests with Playwright
The top of the pyramid drives a real browser against the deployed application, so it also covers the host page, the linker output, the servlet, the session and navigation. The page about E2E testing in real browsers compares Selenium, Playwright and Cypress and covers installation, headless runs, evidence capture and CI; this section only adds what is specific to a GWT application. A separate site-wide issue (#258) tracks Playwright, Cypress and Selenium coverage.
Start the application as in production (the Bookshelf runs from the Jetty plugin on port 9954 in these examples), then run a Playwright Test spec:
mvn -B verify -DskipTests
mvn -B org.eclipse.jetty.ee10:jetty-ee10-maven-plugin:12.1.10:run -pl bookshelf-server -Djetty.http.port=9954 &
npx playwright test
const { test, expect } = require('@playwright/test');
test('the catalogue lists the first page of books', async ({ page }) => {
await page.goto('/');
await expect(page.getByText('1-10 of 25')).toBeVisible();
await expect(page.getByRole('row', { name: /^Effective Java Joshua Bloch/ })).toBeVisible();
});
test('opening a book changes the history token', async ({ page }) => {
await page.goto('/');
await page.getByRole('row', { name: /^Effective Java Joshua Bloch/ }).getByRole('button', { name: 'Open' }).click();
await expect(page).toHaveURL(/#book:/);
});
Running 2 tests using 1 worker
✓ 1 tests/catalogue.spec.js:3:1 › the catalogue lists the first page of books (119ms)
✓ 2 tests/catalogue.spec.js:9:1 › opening a book changes the history token (174ms)
2 passed (739ms)
The playwright.config.js only sets use.baseURL to the server URL. This run used @playwright/test 1.64.0 and, because
the browser revision it expects was not installed, an executablePath in launchOptions pointing to an already cached
headless Chromium; a normal setup runs npx playwright install chromium once. Playwright for Java (Maven artifact
com.microsoft.playwright:playwright, 1.63.0 on Central) has the same locators and suits teams that want the E2E tests next
to the server tests; the choice does not change anything on the GWT side.
What is specific to GWT:
-
Wait for the application, not for the page. The host page loads
bookshelf.nocache.js, which loads the permutation, which creates the widgets, then the catalogue is fetched over RPC.page.gotoreturns long before. The auto-waiting assertions (toBeVisibleon the text1-10 of 25) wait for the rendered result; sleeps are not needed. -
Locate by role and text, never by class names. The generated CSS class names of an obfuscated GWT application (
CFEWP3D-b-ain the Bookshelf’s compiledDataGrid) change with every compile. TheDataGridrows exposerole="row"and the buttons their text, which is what the spec uses. -
Make the data selectors unambiguous. The Bookshelf seeds 25 books, so
Effective Javaappears twice (Effective Java (copy 2)), and the first draft of the spec failed with a strict mode violation (section Pitfalls). -
History tokens are part of the contract.
toHaveURL(/#book:/)tests that the place changed, which is the observable effect of the activity and place wiring (see History and navigation). -
Security features run for real here. The XSRF cookie and token, the
Content-Security-Policyheader and the session filter are only exercised end to end; a failing journey after a security change is the expected signal, not a flaky test. Do not disable XSRF protection to make the test pass.
Choosing what to run when
| When | Run | Why |
|---|---|---|
Every save (IDE) |
Plain JUnit tests of the module you are editing |
Milliseconds. |
Before commit |
|
All 19 JUnit tests, including the suite. Takes the GWT compile of each test module once |
Debugging a failing |
|
Java breakpoints in the first case, the real browser in the second |
Release candidate |
Playwright against the packaged application, on at least Chromium and Firefox |
Catches what HtmlUnit cannot: layout, real browser differences, the deployed layout (see the page about server integration and deployment) |
The pages Compile Reports & Optimization, Linkers & Bootstrap and Code Splitting each show checks that belong to the build rather than the test suite. The Tooling & IDE Support page covers running these tests from IntelliJ IDEA and Eclipse.
Pitfalls
Every message below was produced in the Bookshelf scratch application on JDK 21 (or, where marked, found in the 2.13.1 sources).
| Symptom | Cause | Fix |
|---|---|---|
|
No |
Set |
|
|
Run |
|
The test module still has several permutations (here 3 locales times 2 user agents) and |
Pin |
|
Two |
Group them in a |
|
The page never reached the server: a Manual run with no browser, or |
Open the printed URL, drop |
|
|
Raise the delay well above the expected time, and check that the callback runs |
|
|
Call |
|
A plain JUnit test sits in a package of the module source path and the compiler tries to translate it |
Move plain JUnit tests to a package outside |
|
The suite class is inside the module source path |
Put suites outside it ( |
|
The 2.10 release notes list Safari; the 2.13.1 map has four names |
Use |
|
|
Use it once to read the options |
|
Classic Dev Mode reflects into the JDK |
|
|
The class under test calls |
Make it a |
|
HtmlUnit has no layout |
|
|
|
Define an empty |
JaCoCo reports almost no coverage for the client module |
|
Move logic into plain Java and judge it from the JUnit layer |
|
The data has |
Anchor the name ( |
Found in the sources. The Selenium run style logs |
Deprecated in 2.13 (issue #10222) |
|
Where older material is outdated
-
The Testing page says the tests run in development mode unless
-prodis given. In 2.13.1 production mode is the default (above). The same page, its tutorial and the Eclipse launch files assumewebAppCreatorwith-junit, which generatedFooTest-dev.launchandFooTest-prod.launchfiles and an Antbuild.xml; a Maven project does not use them. -
@DoNotRunWith(Platform.HtmlUnit), shown in the HtmlUnit page and the release notes of 2.0, does not exist: the enum holdsHtmlUnitBug,HtmlUnitLayout,HtmlUnitUnknown,DevelandProd. -
The HtmlUnit page names Firefox 38 emulation and the
FF38,IE11argument. The names areFF,Chrome,IEandEdge. -
The Coverage page (EMMA, EclEmma, a patched
emma.jar,ant devmode) is obsolete for Maven builds; use JaCoCo on the plain JUnit layer. -
The Remote Testing page (Selenium RC servers, the Firefox profile with the GWT Developer Plugin, RemoteWeb) describes mechanisms that are removed (RemoteWeb, the plug-in) or deprecated (Selenium).
-
The book chapter on testing and deploying (JUnit basics,
GWTTestCaseandgetModuleName, asynchronous tests withdelayTestFinish/finishTest) was written for JUnit 3, GWT 1.x and the hosted-mode browser. The core API it teaches is unchanged; the run configuration, the browsers and the hosted mode are not. Its chapter parts on organising the compiled output and installing RPC servlets in a servlet container are covered, for 2.13, on the page about server integration and deployment.
Recent changes
-
2.13.0 added
-strictto the test arguments (the release notes say it helps to find compile issues in GWT libraries) and deprecatedRunStyleSeleniumfor removal (issue #10222, opened 2025-12-10 and closed). -
2.10.0 updated the test runner to HtmlUnit 2.55.0 and Jetty 9.4.44 (the 2.13.1 BOM pins Jetty 9.4.58), which brought
Promisesupport to unit tests. -
2.8.0 (RC1) deprecated classic Dev Mode and kept
-devModefor tests as a temporary switch. -
2.14 (milestone, open): issue #10038, enable
-strictfor user tests, so a stricter default is possible later.