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

flowchart TB e2e["End to end
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.

Table 1. What each layer can and cannot check
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 GWT.create

Anything that needs the compiler: deferred binding (GWT.create), JSNI, JsInterop exports, widgets that touch the DOM

GWTTestCase (HtmlUnit)

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, Timer and RPC round trips

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:

AppTest.gwt.xml
<?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>
BookshelfGwtTest.java (class header and two of the three tests)
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

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

sequenceDiagram participant M as gwt:test (forked JVM) participant S as JUnitShell participant J as Embedded Jetty 9 participant B as HtmlUnit browser participant R as GWTRunner (in the page) M->>S: GWTTestCase.run() calls JUnitShell.runTest() S->>S: compile synthetic module AppTest.JUnit S->>J: start the web server, serve junit.html and the module servlets S->>B: launch the run style on the module URL B->>J: load the compiled JavaScript R->>J: RPC asks for the next block of tests J-->>R: test class and method names R->>R: new test instance, gwtSetUp, test method, gwtTearDown R->>J: RPC reports the result or the exception S-->>M: TestResult passed, failed or errored

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 return null. The test then runs as a plain JUnit test on the JVM ("pure Java mode"), isPureJava() returns true, and gwtSetUp/gwtTearDown still 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.

The test-only service (PingService.java, PingServiceAsync.java and PingServiceImpl.java)
@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; }
}
CatalogAsyncGwtTest.java (the asynchronous tests)
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

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) (class com.google.gwt.junit.DoNotRunWith, enum com.google.gwt.junit.Platform) skips a test class or method on run styles that cannot run it. The enum has exactly Devel, HtmlUnitBug, HtmlUnitLayout, HtmlUnitUnknown and Prod. The HtmlUnit run style bans the three HtmlUnit* 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 shows Platform.HtmlUnit, which does not exist in 2.13.1.

  • @WithProperties(@Property(name = "locale", value = "es")) (class com.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.

CatalogAsyncGwtTest.java (the last two tests)
@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.

AllGwtTests.java (in a package outside the module source path)
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:

Table 2. GWT-specific parameters of gwt:test that the examples use
Parameter Property Effect

testArgs

none (a list in the POM)

Arguments passed to JUnitShell, the same ones as -Dgwt.args

compilerArgs, useCompilerArgsForTests

none

Compiler arguments, prepended to testArgs only when useCompilerArgsForTests is true (default false) and only if they are valid for JUnitShell

enableAssertions

enableAssertions

Default true; turns on -ea for the forked JVM, which activates GWT’s internal consistency checks

sourceLevel, style, draftCompile, optimize, localWorkers, logLevel

maven.compiler.source, gwt.style, gwt.draftCompile, gwt.optimize, gwt.localWorkers, gwt.logLevel

Compiler settings for the test module

port, codeServerPort

gwt.port, gwt.codeServerPort

Documented as the ports of the embedded server and the code server (automatic by default)

outDir, deploy, workDir

none

Output under target/gwt-tests/www, target/gwt-tests/deploy and target/gwt/work

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:

Table 3. JUnitShell options printed by -Dgwt.args=-help in 2.13.1 (those that matter for tests)
Option Meaning

-runStyle runstyle[:args]

Which browser launcher to use (below). The default is HtmlUnit

-[no]devMode

Run the tests as Java bytecode in the JVM. Default off: tests run as compiled JavaScript. The aliases -prod and -web switch it off

-[no]failOnError

Fail compilation when any input file has an error. -strict is a documented alias; default off

-setProperty name=value,value...

Pin a GWT property for the whole run

-userAgents agents

Limit the user agents (permutations), for example safari,gecko1_8

-testMethodTimeout minutes

Timeout of one test method (the shell’s default is 5)

-testBeginTimeout minutes

Time allowed for the browser to contact the server (the shell’s default is 1)

-Xtries n

EXPERIMENTAL: attempts per test method

-[no]showUi

Show the log window and the browser windows (alias -notHeadless)

-[no]checkAssertions, -[no]draftCompile, -optimize, -style, -sourceLevel, -localWorkers, -[no]incremental, -logLevel, -war, -workDir

The compiler options of the same names, applied to the test module

-port, -codeServerPort

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:

Table 4. Run styles in the 2.13.1 source
Run style Argument Verified result

HtmlUnit (default)

Comma separated browsers: FF (default), Chrome, IE, Edge

-runStyle HtmlUnit:Chrome and -runStyle HtmlUnit:FF,Chrome passed 3 of 3 tests. HtmlUnit:Safari is rejected (below)

Manual

Number of browsers to wait for, for example Manual:1

Printed Please navigate your browser to this URL: and a junit.html URL on a random port; opening it in Chrome ran the tests, 3 of 3 passed

ExternalBrowser

Comma separated executables, launched with the test URL as the only argument

ExternalBrowser:/path/to/chrome-headless-shell ran the 3 tests in a real headless Chrome and the process ended with the run

Selenium

host:port/*browser targets of a Selenium RC server

Not run. The constructor logs that it is deprecated for removal, with the pointer to issue #10222

  • HtmlUnit is the default and the only one that needs nothing installed. The default emulation reported itself as Firefox (Firefox/94.0 in the user agent); the official page still says Firefox 38 and FF38,IE11, names that no longer exist. The accepted names are the nicknames of HtmlUnit 2.55.0’s browser versions. HtmlUnit:Safari fails with RunStyleHtmlUnit: Unknown browser name Safari, expected browser name: one of [FF, Chrome, IE, Edge], although the 2.10 release notes list Safari. Chrome and Edge select the safari user-agent permutation of the test module, FF and IE the gecko1_8 one, so a test module that pins user.agent to 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 -testBeginTimeout applies) 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 BrowserManagerServer and the rmi:// 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()"):

CatalogPresenterTest.java (fakes written by hand, no mocking library)
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:

CatalogServiceImplTest.java
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):

SharedTypesTest.java (the reflective test)
@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:

Table 5. JaCoCo result for the Bookshelf (19 JUnit tests, 8 of them GWTTestCase)
Module Instructions covered Observation

bookshelf-shared

52 of 101

The three plain JUnit tests

bookshelf-server

287 of 469

CatalogRepository 224 of 241, CatalogServiceImpl 22 of 35, SessionFilter not covered

bookshelf-client

47 of 836

Only CatalogPresenter (20 of 20 plus its anonymous callback 27 of 27)

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
tests/catalogue.spec.js
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.goto returns long before. The auto-waiting assertions (toBeVisible on the text 1-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-a in the Bookshelf’s compiled DataGrid) change with every compile. The DataGrid rows expose role="row" and the buttons their text, which is what the spec uses.

  • Make the data selectors unambiguous. The Bookshelf seeds 25 books, so Effective Java appears 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-Policy header 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. mvn -B test -pl bookshelf-server is enough for the server; the client needs verify (see the pitfalls)

Before commit

mvn -B verify -Dgwt.args=-strict

All 19 JUnit tests, including the suite. Takes the GWT compile of each test module once

Debugging a failing GWTTestCase

-Dgwt.args=-devMode (with the --add-opens flags) or -runStyle Manual:1 and a real browser’s developer tools

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).

Table 6. Common problems when testing GWT code
Symptom Cause Fix

gwt:test prints its header and nothing else, BUILD SUCCESS, no T E S T S

No includes configured for the goal in this setup

Set <includes> (for example **/*Test.java) or pass -Dtest=...

No source code is available for type com.example.bookshelf.shared.Book; did you forget to inherit a required module?, then JUnitFatalLaunch The test class '...BookshelfGwtTest' had compile errors; check log for details

mvn test was used and the sources classifier JAR of the shared module is only produced at package

Run mvn verify (or install)

java.lang.AssertionError at com.google.gwt.dev.Permutation.assertSameAnswers(Permutation.java:111)

The test module still has several permutations (here 3 locales times 2 user agents) and gwt:test enables assertions

Pin locale and user.agent in the test module, pass -setProperty in -Dgwt.args, or (last resort) -DenableAssertions=false

could not instantiate the requested class with Caused by: ... (TypeError) : Cannot call method "new" of undefined, for some test classes only

Two GWTTestCase classes with the same getModuleName() ran back to back. The page compiled for the first one does not contain the second

Group them in a GWTTestSuite and run the suite from includes. A separate module per class works too

TimeoutException: The browser did not contact the server within 60000ms.

The page never reached the server: a Manual run with no browser, or -batch class. The default -testBeginTimeout is 1 minute

Open the printed URL, drop -batch, or use -testBeginTimeout n

TimeoutException: A timeout expired after 300ms elapsed.

delayTestFinish(300) and finishTest() was never reached before it

Raise the delay well above the expected time, and check that the callback runs

IllegalStateException: This test is not in asynchronous mode; call delayTestFinish() first

finishTest() called without delayTestFinish(), or after the test already ended

Call delayTestFinish in the test method first

Line 55: No source code is available for type org.junit.Assert; did you forget to inherit a required module? (with -strict); without -strict nothing is shown

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 <source path> (unit/)

No source code is available for type com.google.gwt.junit.tools.GWTTestSuite (with -strict)

The suite class is inside the module source path

Put suites outside it (suite/)

RunStyleHtmlUnit: Unknown browser name Safari, expected browser name: one of [FF, Chrome, IE, Edge], then Shell failed to start

The 2.10 release notes list Safari; the 2.13.1 map has four names

Use HtmlUnit:FF,Chrome and a real browser for Safari

-Dgwt.args=-help prints usage, then JUnitFatalLaunchException: Error processing shell arguments (and a NullPointerException on runStyle)

-help ends the shell, not the Maven goal

Use it once to read the options

InaccessibleObjectException: ... module java.base does not "opens java.lang" to unnamed module after 60 s, only with -devMode

Classic Dev Mode reflects into the JDK

--add-opens java.base/java.lang=ALL-UNNAMED and --add-opens java.base/java.util=ALL-UNNAMED in argLine, or stay in production mode

java.lang.UnsupportedOperationException: ERROR: GWT.create() is only usable in client code! in a plain JUnit test

The class under test calls GWT.create (directly or in a field initialiser)

Make it a GWTTestCase, inject the instance, or use GWTMockUtilities.disarm() plus a mock

expected: <true>, actual: <false> on getOffsetHeight() > 0 only under HtmlUnit

HtmlUnit has no layout

@DoNotRunWith(Platform.HtmlUnitLayout) and cover it in Playwright

The forked VM terminated without properly saying goodbye. VM crash or System.exit called? with '@{argLine}' in the printed command

<argLine>@{argLine} ...</argLine> without any plugin setting the property (no JaCoCo in this build)

Define an empty <argLine></argLine> property in the POM

JaCoCo reports almost no coverage for the client module

GWTTestCase code runs as JavaScript (or in a class loader the agent does not instrument)

Move logic into plain Java and judge it from the JUnit layer

strict mode violation: getByRole('row', { name: /Effective Java/ }) resolved to 2 elements in Playwright

The data has Effective Java and Effective Java (copy 2)

Anchor the name (/^Effective Java Joshua Bloch/) or use a test data set without duplicates

Found in the sources. The Selenium run style logs The Selenium run style is deprecated for removal. The sources can be copied into your own project to continue using it.

Deprecated in 2.13 (issue #10222)

ExternalBrowser, Manual or Playwright instead

Where older material is outdated

  • The Testing page says the tests run in development mode unless -prod is given. In 2.13.1 production mode is the default (above). The same page, its tutorial and the Eclipse launch files assume webAppCreator with -junit, which generated FooTest-dev.launch and FooTest-prod.launch files and an Ant build.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 holds HtmlUnitBug, HtmlUnitLayout, HtmlUnitUnknown, Devel and Prod.

  • The HtmlUnit page names Firefox 38 emulation and the FF38,IE11 argument. The names are FF, Chrome, IE and Edge.

  • 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, GWTTestCase and getModuleName, asynchronous tests with delayTestFinish/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 -strict to the test arguments (the release notes say it helps to find compile issues in GWT libraries) and deprecated RunStyleSelenium for 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 Promise support to unit tests.

  • 2.8.0 (RC1) deprecated classic Dev Mode and kept -devMode for tests as a temporary switch.

  • 2.14 (milestone, open): issue #10038, enable -strict for user tests, so a stricter default is possible later.