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 a running FX
toolkit, 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 is meant to be declared
<scope>test</scope>in downstream projects — never as a compile/runtime dependency. It bringstentackle-fx-rdcandtentackle-test-pdoalong. None of the test frameworks arrives transitively (TestNG and the Jupiter API areoptional), and the scripting language and the JDBC driver are test scope here, so the downstream project declares them itself.
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 its per-class application lifecycle (open the session,
create & populate the in-memory schema, register the application, teardown, skip-when-no-backend) and adding only
what the UI needs on top. Like TestApplication, it has no automatic transaction handling:
org.tentackle.app.AbstractApplication (tentackle-pdo)
▲
TestApplication (tentackle-test-pdo) ── DB session, schema, registration
▲
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 withPlatform.startup(...). A toolkit that is already running is accepted. If the JVM is headless (GraphicsEnvironment.isHeadless()) or FX is unavailable, it throws TestNG'sSkipExceptionso the suite is skipped, not failed in CI without a display. The JUnit variant cannot use a@BeforeAllmethod here, because JUnit runs a superclass's@BeforeAllmethods first. The session would already be open and the in-memory database populated, and when the class aborted, that leaked connection would keep the database alive for the next class. Instead, the nestedFxExtension(aBeforeAllCallback, which JUnit runs before every@BeforeAllmethod) calls the idempotentstartFx(). A headless JVM or missing FX support raisesTestAbortedException, and the tests are reported as skipped. The failure is remembered, so the remaining classes are aborted without trying again.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 insidePlatform.runLater, so code that runs on the UI thread uses a session of its own instead of sharing the test thread's. 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 on the calling (test) thread, then posts aCountDownLatchcountdown to the FX thread viaPlatform.runLater, and waits (default 30 s, seesetFxTimeout) until it was processed — i.e. until all FX events queued by the test so far have been handled. A runtime exception from the body, a timeout, or a captured FX-thread exception fails the test. This is how a synchronous TestNG/JUnit assertion can reliably wait for asynchronous FX work. Note that a captured FX-thread exception is not cleared, so it fails every subsequentrunFxof the same test as well.unregister()override — unregisters the application and closes the FX-thread session on teardown. If the FX thread was never prepared (no FX-thread session), the application was never registered either, and nothing happens.
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.
All checks of a test class run in a single test method, so the first failure ends the run.
ControllerTest¶
Verifies that every FxController can be loaded and bound. For
each controller class from FxFactory.getInstance().getControllerClasses():
Fx.load(clazz)— loads the FXML and instantiates the controller;controller.validateInjections()— asserts all@FXML/injected members are present;- 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 — honoring @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, honoring @TableConfigurationProviderService(test=…),
calls createTableConfiguration() inside runFx.
Common conventions¶
| Feature | Behavior |
|---|---|
| Package prefix | Each base class takes an optional packagePrefix constructor argument, matched against the controller or provider class name. 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 / the JUnit TestReporter 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
for TestNG and org.tentackle.test.fx.rdc.junit for JUnit. The JUnit package also contains ApplicationTest,
which runs a plain JUnit TestApplication with more than one test method. These tests double as the canonical
usage reference. Database connection parameters come from
backend.properties, which selects the in-memory H2 database, inherited from the
test-pdo session machinery. Unlike
test-pdo's, the URL deliberately carries no DB_CLOSE_DELAY=-1: TestApplication opens its session and
creates the schema per test class, and H2 discards an in-memory database when its last connection closes, so
each class starts from an empty one. Keeping the database alive across classes would make the second
CREATE SEQUENCE fail.
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.⚠️ The surefire provider has to be pinned¶
This module puts both TestNG and JUnit Jupiter on the classpath, because it offers a base class for each. Surefire's auto-detection then picks only the JUnit platform provider. TestNG tests are silently not run: the module reports
Tests run: 0while the build stays green. The POM therefore declares bothsurefire-testngandsurefire-junit-platformas plugin dependencies, exactly as test-pdo does. Surefire runs each provider in its own forked JVM, and the POM addsjunit-jupiter-engineandjunit-platform-launcherin test scope. A downstream project that has both frameworks on its test classpath has to pin the providers the same way. A downstream JUnit project needsjunit-jupiter-engineand ajunit-platform-launcherof the same version in test scope; otherwise surefire's bundled launcher does not match the engine and test discovery fails.
Related Documentation¶
- tentackle-test-pdo — the persistence-layer test-support module this one builds on.
- Rich Data Components (RDC) —
GuiProvider, table/tree/editor/finder views under test. - Tentackle FX —
FxController,FxFactory, binding and@FxControllerService. - PDO — Persistent Domain Objects — the domain objects the GUI providers render.
- Common / ServiceFinder SPI — the discovery mechanism the generic tests iterate.