Skip to content

Tentackle FX RDC Tests — the tentackle-test-fx-rdc Module

Overview

tentackle-test-fx-rdc is the test-support module for the UI layer. It is the RDC counterpart of tentackle-test-pdo: where that module gives you base classes to test persistent domain objects against a database, this one gives you base classes to smoke-test the JavaFX front end — controllers, GUI providers, and table-configuration providers — on a real FX toolkit, with a live session underneath.

Its central idea is the generic coverage test. Rather than write one test per screen, you extend one of three abstract base classes and the test automatically discovers every @FxControllerService controller, GuiProvider or TableConfigurationProvider registered via the ServiceFinder SPI, instantiates each one on the FX thread, and asserts it loads, binds, and renders without error. A single one-line subclass thus guards your whole UI against broken FXML, unbound fields, missing resources, or refactoring drift.

Test scope only. Like its PDO sibling, this artifact pulls in TestNG, JUnit, Groovy, and PostgreSQL and is meant to be declared <scope>test</scope> in downstream projects — never as a compile/runtime dependency.

The module is not a JPMS module: it has no module-info.java; the jar carries Automatic-Module-Name: org.tentackle.fx.rdc.test and lives on the test classpath, where SPI discovery works without an explicit module descriptor.


How It Builds on tentackle-test-pdo

This module depends on tentackle-test-pdo and tentackle-fx-rdc. The base class FxRdcTestApplication extends the PDO module's TestApplication, inheriting the entire database/session/transaction lifecycle (open session, create & populate the in-memory schema, modification tracker, teardown, skip-when-no-backend) and adding only what the UI needs on top:

   org.tentackle.app.AbstractApplication           (tentackle-pdo)
   TestApplication                                  (tentackle-test-pdo)  ── DB session, schema, transactions
   FxRdcTestApplication                             (tentackle-test-fx-rdc) ── JavaFX toolkit + FX-thread session
   ┌──────────┼───────────────────────────┐
   ControllerTest   GuiProviderTest   TableConfigurationProviderTest

So an FX-RDC test is a fully bootstrapped Tentackle application plus a started FX toolkit.


Package Layout

Package Role
org.tentackle.fx.rdc.testng TestNG base classes: FxRdcTestApplication, ControllerTest, GuiProviderTest, TableConfigurationProviderTest. The default framework.
org.tentackle.fx.rdc.junit JUnit 5 equivalents of the same four classes, for projects that prefer Jupiter.

The two packages are mirror images; pick the one matching your test runner. The descriptions below use the TestNG names.


FxRdcTestApplication — the FX bootstrap

The abstract base of all three test types. It adds the JavaFX dimension to the inherited TestApplication:

  • @BeforeSuite startFx() — starts the JavaFX toolkit with Platform.startup(...). If the JVM is headless (GraphicsEnvironment.isHeadless()) or FX is unavailable, it throws TestNG's SkipException so the suite is skipped, not failed in CI without a display.
  • setDomainContext(...) override — when the inherited context is set, it clones the session for the FX thread (context.getSession().clone("FX")) and makes that clone the current session inside Platform.runLater, so controllers that touch persistence from the UI thread work correctly. It also installs an uncaught-exception handler on the FX thread so exceptions thrown asynchronously during rendering are captured and reported.
  • runFx(name, runnable) — the core helper. It runs the test body, then posts a CountDownLatch countdown to the FX thread, and waits (default 30 s, see setFxTimeout) until the FX event queue has drained — guaranteeing all FX processing triggered by the test has completed. Any captured FX-thread exception fails the test. This is how a synchronous TestNG/JUnit assertion can reliably wait for asynchronous FX work.
  • unregister() override — closes the FX-thread session on teardown.

The FX timeout is configurable per test via setFxTimeout(seconds).


The Three Generic Tests

Each test iterates the relevant SPI registry, filters by an optional package prefix (so a project can restrict the run to its own classes), honors a per-service opt-out, and exercises each implementation inside runFx.

ControllerTest

Verifies that every FxController can be loaded and bound. For each controller class from FxFactory.getInstance().getControllerClasses():

  1. Fx.load(clazz) — loads the FXML and instantiates the controller;
  2. controller.validateInjections() — asserts all @FXML/injected members are present;
  3. unless the controller opts out of binding, controller.getBinder().assertAllBound() — asserts every bound field actually resolved.

The @FxControllerService annotation drives opt-out: test() == false skips the controller entirely, and binding() == BINDING.NO skips the bind assertion. This catches the classic failure modes — renamed FXML ids, missing resource bundles, fields bound to non-existent properties — across the whole UI at once.

GuiProviderTest

Verifies that every GuiProvider can produce its views. It maps each serviced PDO class (via ClassMapper.create(name, GuiProvider.class)), creates the PDO, obtains its GuiProvider, and — honouring @GuiProviderService(test=…) — exercises the full set of view factories: createEditor() / createFinder() (when available), createGraphic(), createTableView(), createTreeItem(). A PDO with no provider (ClassNotFoundException cause) is reported and skipped rather than failed.

TableConfigurationProviderTest

Verifies that every TableConfigurationProvider can build its table configuration. For each serviced class it obtains the provider from TableConfigurationProviderFactory and, honouring @TableConfigurationProviderService(test=…), calls createTableConfiguration() on the FX thread.

Common conventions

Feature Behaviour
Package prefix Each base class takes an optional packagePrefix constructor argument; only matching classes are tested. null/empty = all.
Opt-out The matching @*Service(test=false) annotation excludes an implementation; it is logged as "not tested" rather than run.
Discovery Implementations are found through the SPI registries (FxFactory, ClassMapper, the provider factories), so nothing has to be listed by hand.
Reporting Every step is written to the TestNG Reporter / JUnit log for a readable per-class trace.

Using It in a Downstream Project

Add the dependency in test scope:

<dependency>
  <groupId>org.tentackle</groupId>
  <artifactId>tentackle-test-fx-rdc</artifactId>
  <version>${tentackle.version}</version>
  <scope>test</scope>
</dependency>

Then a complete UI smoke-test suite is just three one-line subclasses in a test source folder:

public class TestControllers                 extends ControllerTest {}
public class TestGuiProviders                extends GuiProviderTest {}
public class TestTableConfigurationProviders extends TableConfigurationProviderTest {}

To restrict a run to your own packages, call the prefix constructor:

public class TestGuiProviders extends GuiProviderTest {
  public TestGuiProviders() {
    super("com.example.myapp");
  }
}

These are exactly the example tests shipped in this module's own src/test/java (org.tentackle.test.fx.rdc), which double as the canonical usage reference. Database connection parameters come from the filtered backend.properties (dbUrl/dbUser/dbPasswd build properties), inherited from the test-pdo session machinery.

Because the tests start a real JavaFX toolkit, they self-skip on headless build agents. To run them in CI, you need a display (or a virtual one such as Xvfb); otherwise the suite reports as skipped, not failed.