Skip to content
CMO & CTO
CMO & CTO

Closing the Bridge Between Marketing and Technology, By Luis Fernandez

  • Digital Experience
    • Experience Strategy
    • Experience-Driven Commerce
    • Multi-Channel Experience
    • Personalization & Targeting
    • SEO & Performance
    • User Journey & Behavior
  • Marketing Technologies
    • Analytics & Measurement
    • Content Management Systems
    • Customer Data Platforms
    • Digital Asset Management
    • Marketing Automation
    • MarTech Stack & Strategy
    • Technology Buying & ROI
  • Software Engineering
    • Software Engineering
    • Software Architecture
    • General Software
    • Development Practices
    • Productivity & Workflow
    • Code
    • Engineering Management
    • Business of Software
    • Code
    • Digital Transformation
    • Systems Thinking
    • Technical Implementation
  • About
CMO & CTO

Closing the Bridge Between Marketing and Technology, By Luis Fernandez

Testing GWT UIs: Strategies that Survive Refactors

Posted on October 13, 2012 By Luis Fernandez

Are your GWT UI tests snapping every time you rename a widget, shuffle a panel, or touch a UiBinder file? Do you tiptoe around refactors because your test suite is ready to throw a tantrum at the slightest change? You are not alone.

Good news: you can make them stick.

What breaks and why

Most GWT UI tests fail not because the feature is wrong but because the test was glued to something fragile. CSS class names change when you switch to CssResource. DOM structure shifts when you nest a new panel. A field gets a new name in UiBinder. The test keeps staring at the furniture while the behavior lives elsewhere. If you want tests that survive refactors, you need to pin them to behavior and to stable hooks you control. That is the core idea. Everything else is tactics.

Let’s turn flakiness into signal.

Rule 1: test behavior in presenters, not pixels

GWT gives you a nice split with MVP, Activities and Places, UiBinder, and an EventBus. Push logic into a Presenter or Activity with a View interface. Keep the view dumb. Test the presenter in the regular JVM with JUnit and Mockito. No browser. No GWTTestCase. No Dev Mode. Fast and refactor friendly. When you move widgets around, the presenter test keeps passing because it never cared about the layout.

Keep pixels for a smaller set of smoke tests.

public interface LoginView {
  void setPresenter(Presenter p);
  String getUser();
  String getPassword();
  void showError(String msg);
  interface Presenter {
    void onLogin();
  }
}

public class LoginPresenter implements LoginView.Presenter {
  private final LoginView view;
  private final AuthService auth;
  private final EventBus bus;

  public LoginPresenter(LoginView view, AuthService auth, EventBus bus) {
    this.view = view;
    this.auth = auth;
    this.bus = bus;
    this.view.setPresenter(this);
  }

  @Override
  public void onLogin() {
    String user = view.getUser();
    String pass = view.getPassword();
    if (user == null || user.isEmpty()) {
      view.showError("User required");
      return;
    }
    if (auth.login(user, pass)) {
      bus.fireEvent(new LoggedInEvent(user));
    } else {
      view.showError("Bad credentials");
    }
  }
}
// JRE unit test
public class LoginPresenterTest {
  @Test public void loginSuccessFiresEvent() {
    LoginView view = mock(LoginView.class);
    when(view.getUser()).thenReturn("ana");
    when(view.getPassword()).thenReturn("secret");
    AuthService auth = mock(AuthService.class);
    when(auth.login("ana", "secret")).thenReturn(true);
    EventBus bus = mock(EventBus.class);

    LoginPresenter p = new LoginPresenter(view, auth, bus);
    p.onLogin();

    verify(bus).fireEvent(isA(LoggedInEvent.class));
    verify(view, never()).showError(anyString());
  }
}

This is the kind of test that lives through a full UiBinder rewrite.

Rule 2: give widgets stable hooks you control

When you do need to click buttons and type in fields, do not point WebDriver at CSS classes or absolute XPaths. CssResource obfuscates class names, theme tweaks change markup, and your test goes down. Instead, give each interesting element a stable hook. GWT has UIObject.ensureDebugId, which is pretty handy. You can also set a custom attribute like dataHook or simply an id with a shared constant. The key is to pick a scheme and never tie it to visual styling.

Make hooks speak intent, not layout.

public final class Qa {
  private Qa() {}
  public static final String LOGIN_USER = "loginUser";
  public static final String LOGIN_PASS = "loginPass";
  public static final String LOGIN_SUBMIT = "loginSubmit";
}

// UiBinder view snippet
public class LoginViewImpl extends Composite implements LoginView {
  interface Binder extends UiBinder<Widget, LoginViewImpl> {}
  @UiField TextBox user;
  @UiField PasswordTextBox pass;
  @UiField Button submit;

  @Inject
  public LoginViewImpl(Binder binder) {
    initWidget(binder.createAndBindUi(this));
    // Stable hooks
    user.getElement().setAttribute("dataHook", Qa.LOGIN_USER);
    pass.getElement().setAttribute("dataHook", Qa.LOGIN_PASS);
    submit.getElement().setAttribute("dataHook", Qa.LOGIN_SUBMIT);
  }
}

Notice the use of constants. If a name changes, you change it in one place and the test code keeps compiling with help from the IDE.

Rule 3: page objects that speak your domain

Wrap your screens with a Page Object. Page Objects give you a single vocabulary for the actions that matter. They hide selectors and DOM trivia from tests. When markup changes, you change one class. The more the page object reads like your product, the better your tests survive.

Keep locators short and focused.

public class LoginPage {
  private final WebDriver driver;

  public LoginPage(WebDriver driver) {
    this.driver = driver;
  }

  private By byHook(String value) {
    return By.cssSelector("[dataHook='" + value + "']");
  }

  public LoginPage typeUser(String user) {
    driver.findElement(byHook(Qa.LOGIN_USER)).clear();
    driver.findElement(byHook(Qa.LOGIN_USER)).sendKeys(user);
    return this;
  }

  public LoginPage typePass(String pass) {
    driver.findElement(byHook(Qa.LOGIN_PASS)).clear();
    driver.findElement(byHook(Qa.LOGIN_PASS)).sendKeys(pass);
    return this;
  }

  public HomePage submit() {
    driver.findElement(byHook(Qa.LOGIN_SUBMIT)).click();
    return new HomePage(driver);
  }
}

Tests now read like a story and do not care if you switched from FlowPanel to LayoutPanel.

Rule 4: protect navigation with place tests

Activities and Places let you model screens and history tokens. When you refactor tokens or change how places map to screens, you want quick feedback that deep links still work. Write small tests around your PlaceHistoryMapper and Tokenizer code. These are JVM fast and catch subtle bugs.

Routing should be boring and reliable.

public class PlaceMappingTest {
  private final PlaceHistoryMapper mapper = GWT.create(AppPlaceHistoryMapper.class);

  @Test public void tokenRoundTrip() {
    DashboardPlace place = new DashboardPlace("inbox");
    String token = mapper.getToken(place);
    assertThat(token).contains("inbox");

    Place parsed = mapper.getPlace(token);
    assertTrue(parsed instanceof DashboardPlace);
    assertEquals("inbox", ((DashboardPlace) parsed).getSection());
  }
}

When a product owner asks for a new token format, you change the mapper and keep your test as a safety net.

Rule 5: event bus contracts

The event bus is glue. Glue can get messy. Write tests that assert which events fire and which do not. Do not guess. When you move logic across presenters, the tests continue to verify that the same user actions yield the same events.

Events are part of your public story.

public class ProfilePresenterTest {
  @Test public void savingProfileFiresSavedEvent() {
    ProfileView view = mock(ProfileView.class);
    when(view.getName()).thenReturn("Rafa");
    EventBus bus = new SimpleEventBus();
    final AtomicBoolean fired = new AtomicBoolean(false);

    bus.addHandler(ProfileSavedEvent.TYPE, e -> fired.set(true));
    ProfileService svc = mock(ProfileService.class);
    when(svc.save(any(Profile.class))).thenReturn(true);

    ProfilePresenter p = new ProfilePresenter(view, svc, bus);
    p.onSave();

    assertTrue(fired.get());
  }
}

This keeps the contract clear even while code shifts around it.

Rule 6: GWTTestCase is a spice, not the meal

GWTTestCase spins up a special environment and can be slow. Use it only when you must render real widgets and test client code that needs the GWT runtime. For everything else, write pure JVM tests. Your future self will thank you when your suite finishes before that second coffee.

Speed breeds trust.

public class OnlyWhenNeededTest extends GWTTestCase {
  @Override public String getModuleName() {
    return "com.example.App";
  }

  public void testWidgetRendersError() {
    LoginViewImpl view = new LoginViewImpl(new LoginViewImpl.Binder(){...});
    view.showError("Oops");
    assertEquals("Oops", view.getErrorLabel().getText());
  }
}

Keep these few and focused, and your build stays friendly.

Rule 7: tame time and async

Timers, Scheduler, and async RPC can make tests flaky. Wrap time behind a small interface so you can control it. For async, keep the logic in the presenter and drive it with fakes or callbacks you can trigger in tests. The goal is to make the test decide when the world moves.

Determinism beats sleep calls.

public interface Clock {
  double nowMillis();
  void schedule(int delayMillis, Runnable r);
}

public class GwtClock implements Clock {
  @Override public double nowMillis() { return Duration.currentTimeMillis(); }
  @Override public void schedule(int delay, Runnable r) {
    new Timer() { public void run() { r.run(); } }.schedule(delay);
  }
}

// In tests
public class FakeClock implements Clock {
  private final Queue<Runnable> tasks = new ArrayDeque<>();
  @Override public double nowMillis() { return 42; }
  @Override public void schedule(int delay, Runnable r) { tasks.add(r); }
  public void runAll() { while(!tasks.isEmpty()) tasks.poll().run(); }
}

With a fake clock you can click, advance, and assert without waiting for real time.

Rule 8: run browser tests against compiled code

Dev Mode is handy for debugging, but it is not the same DOM or timing you get in production. For WebDriver, run against the compiled output. The test then sees what users see. You catch script split issues, code splitting latency, and weird browser quirks that never show up in Dev Mode.

Your CI should build once and test that artifact.

# Maven-ish sketch
mvn clean package -Pprod
# Static server pointing to target/App
java -jar tiny-httpd.jar target/App
# WebDriver points to http://localhost:8080/App/App.html
mvn -Dtest=E2eSuite test

Tooling that works right now

GWT 2.5 RC builds are around and they are fast. WebDriver runs solid on Chrome and Firefox. HtmlUnit can drive headless runs for the lab. Mockito and Fest make tests readable. If you use GIN or GWTP you already have clean seams for presenters and views, which makes everything above smoother. The stack is boring in the best way.

Pick tools that fade into the background so your team can focus on behavior.

A tiny checklist for refactor proof GWT tests

– Presenters own behavior and have view interfaces
– Views are dumb and only forward user actions
– Stable hooks on widgets via ensureDebugId, dataHook, or id constants
– Page Objects hide selectors and talk in product language
– Small set of browser tests against compiled output
– Tests for place tokens and history round trips
– Event bus tests for the big user actions
– Fake time and async to remove sleeps
– Rare GWTTestCase, many JVM tests

Use this list during code reviews and you will see fewer broken builds after refactors.

A short example end to end test

To make it concrete, here is how a login story can read in a WebDriver suite with hooks and a page object. Notice there are no CSS classes in sight and no DOM trivia in the test code. The only thing the test cares about is the user story.

Simple to read, simple to fix when things change.

public class LoginStoryTest {
  private WebDriver driver;

  @Before public void openApp() {
    driver = new FirefoxDriver();
    driver.get("http://localhost:8080/App/App.html");
  }

  @After public void quit() {
    driver.quit();
  }

  @Test public void userCanLogin() {
    HomePage home = new LoginPage(driver)
        .typeUser("ana")
        .typePass("secret")
        .submit();

    assertTrue(home.isLoaded());
  }
}

Common pitfalls to avoid

– Pointing tests at CSS classes when using CssResource
– XPaths that mirror the whole layout tree
– Sleep calls instead of waiting for a hook or a state
– Packing logic into views just because it is easy in UiBinder
– Skipping a page object because the test is small today
– Running WebDriver on Dev Mode while your users run compiled code

The whole point is to keep tests loyal to behavior while UI code keeps moving.

Why this survives refactors

Refactors change names, structure, and styling. They rarely change intent. By teasing apart behavior from presentation, by giving yourself stable hooks that are not tied to CSS, and by wrapping screens in page objects, you force your tests to align with the product story. When you rename a widget or redo your UiBinder, all the fast presenter tests pass. When you move a button to a new panel, the end to end test still finds it through a constant based hook. When you change deep link tokens, the mapper tests guide the change. You stop fearing the rename refactor and start using it freely again.

That is how tests become your safety net instead of your ankle weights.

Build tests that describe behavior, not layout.

Development Practices Software Engineering

Post navigation

Previous post
Next post
  • Digital Experience (94)
    • Experience Strategy (19)
    • Experience-Driven Commerce (5)
    • Multi-Channel Experience (9)
    • Personalization & Targeting (21)
    • SEO & Performance (10)
  • Marketing Technologies (92)
    • Analytics & Measurement (14)
    • Content Management Systems (45)
    • Customer Data Platforms (4)
    • Digital Asset Management (8)
    • Marketing Automation (6)
    • MarTech Stack & Strategy (10)
    • Technology Buying & ROI (3)
  • Software Engineering (310)
    • Business of Software (20)
    • Code (30)
    • Development Practices (52)
    • Digital Transformation (21)
    • Engineering Management (25)
    • General Software (82)
    • Productivity & Workflow (30)
    • Software Architecture (85)
    • Technical Implementation (23)
  • 2025 (12)
  • 2024 (8)
  • 2023 (18)
  • 2022 (13)
  • 2021 (3)
  • 2020 (8)
  • 2019 (8)
  • 2018 (23)
  • 2017 (17)
  • 2016 (40)
  • 2015 (37)
  • 2014 (25)
  • 2013 (28)
  • 2012 (24)
  • 2011 (30)
  • 2010 (42)
  • 2009 (25)
  • 2008 (13)
  • 2007 (33)
  • 2006 (26)

Ab Testing Adobe Adobe Analytics Adobe Target AEM agile-methodologies Analytics architecture-patterns CDP CMS coding-practices content-marketing Content Supply Chain Conversion Optimization Core Web Vitals customer-education Customer Data Platform Customer Experience Customer Journey DAM Data Layer Data Unification documentation DXP Individualization java Martech metrics mobile-development Mobile First Multichannel Omnichannel Personalization product-strategy project-management Responsive Design Search Engine Optimization Segmentation seo spring Targeting Tracking user-experience User Journey web-development

©2025 CMO & CTO | WordPress Theme by SuperbThemes