Tentackle PDO Tests — the tentackle-test-pdo Module¶
Overview¶
tentackle-test-pdo is the test-support module for writing
PDO-based tests. It does three distinct jobs:
- Provides test base classes — abstract TestNG and JUnit 5 base classes (
AbstractPdoTest,TestApplication) that open a session, populate an in-memory database, manage the per-class/per-method transaction lifecycle, run the modification tracker and tear everything down again — so a test class only has to write the assertions. - Provides mock delegates — lightweight
Mock*domain/persistence delegates that let you instantiate PDOs and operations in a unit test without a database. - Generates the framework's SQL — at build time it runs the
SQL Maven plugin over the
framework model (number sources, security, preferences, …) and packages the resulting DDL, model dump and
MODEL-INDEX.LISTinto the jar, so downstream tests can create the TT (Tentackle) tables in an in-memory database from the classpath.
Test scope only. This artifact pulls in TestNG, JUnit, H2 and PostgreSQL and (deliberately, see below) marks the framework layers
optionalto keep the classpath order right. It must be included with<scope>test</scope>in downstream projects — never as a compile/runtime dependency.
This module is not a JPMS module. It has no module-info.java; the jar carries the
Automatic-Module-Name: org.tentackle.pdo.test. It is meant to live on the test classpath, where Tentackle's
ServiceFinder SPI discovery still works without an explicit
module descriptor.
Package Layout¶
| Package | Role |
|---|---|
org.tentackle.pdo.testng |
TestNG base classes: AbstractPdoTest and TestApplication. The default framework (the module's own tests run under TestNG via src/test/testng.xml). |
org.tentackle.pdo.junit |
JUnit 5 equivalents of the same two base classes, for projects that prefer Jupiter. |
org.tentackle.pdo.test |
DbTestUtilities — low-level helpers that build and run the database-population SQL script from the model. Shared by both the TestNG and JUnit base classes. |
org.tentackle.pdo.mock |
MockDomainObject, MockPersistentObject, MockDomainOperation, MockPersistentOperation — no-op delegates for database-less unit tests. Ships in the separate tentackle-pdo-mock artifact (see below) and reaches downstream projects through this one. |
The Test Base Classes¶
There are two parallel families, one per test framework, with the same shape. Pick the one that matches your project's test runner; the TestNG variant is the default in Tentackle.
AbstractPdoTest¶
The base class for tests on PDO level — i.e., tests that talk to a live (usually in-memory) database through the
PDO API. It implements DomainContextProvider, so the running test is the context source for the PDOs it
creates.
Transaction handling is declared per test class through the TransactionType enum, chosen via the
constructors:
| Constructor | Transaction | Commit/rollback |
|---|---|---|
AbstractPdoTest() |
CLASS (one transaction per test class) |
rollback |
AbstractPdoTest(TransactionType) |
as given | rollback |
AbstractPdoTest(TransactionType, boolean commit) |
as given | commit if true |
TransactionType is CLASS (transaction spans the whole class), METHOD (one per test method) or NONE (no
automatic transaction). Tests roll back by default so they leave no trace; pass commit = true only for the
rare scenario that must persist.
Lifecycle (TestNG annotations shown; JUnit uses the equivalent @BeforeAll/@AfterAll/…):
@BeforeSuite openSessionsAndStartModificationTracker()— opens the session (Pdo.createSession()), builds theDomainContext, and if the backend is in-memory creates the database tables and populates them. It also picks a default scripting language if exactly one is available, and starts theModificationTracker(with a fast 500 ms poll interval suited to tests).@BeforeClass/@AfterClass— begin/end the transaction whenTransactionType.CLASS.@BeforeMethod/@AfterMethod— begin/end the transaction whenTransactionType.METHOD; a failed method flipsrollbackLoggedso the rolled-back statements are logged for diagnosis.@AfterSuite closeSessionsAndTerminateModificationTracker()— terminates the tracker and closes the session.
Extension points a concrete test can override:
| Method | Purpose |
|---|---|
createDatabaseTables(Db) |
Build the schema. The default loads the model from the classpath via DbTestUtilities and runs the generated script. Override to add extra model sources. |
populateDatabase() |
Insert test fixtures after the tables exist. Default does nothing. |
createDomainContext() |
Supply the (thread-local) DomainContext. Default Pdo.createDomainContext(). |
openSession() |
Open the session; default throws TestNG SkipException (JUnit: aborts) when no backend is reachable, so the suite is skipped, not failed, in an incomplete environment. |
Helpers for richer scenarios: runPreCommits()/runPostCommits() force the pre/post-commit callbacks even
though the test will roll back; setRollbackLogged(true) logs the SQL even on success; runClass(...),
runInOtherJVM(...) and waitForProcess(...) spawn a test class in a separate JVM (used by the remote/RPC
tests — see testng.xml's remote1/remote2 groups).
TestApplication¶
The base class for tests that must run as a full
AbstractApplication — i.e., with the complete application
bootstrap (SessionInfo, property application, registration) rather than just a bare session. Same in-memory
table creation/population, but driven through the application lifecycle. The JUnit variant keeps a static reference
to the running application because JUnit's @AfterAll must be static, and guards against two TestApplications
running at once.
DbTestUtilities — model → DDL → database¶
DbTestUtilities (in org.tentackle.pdo.test, an @Service defaulting to itself, so it is replaceable) is the
low-level bridge from the model to a live database used by the base classes:
createPopulateScript(Db)— loads the default model from the classpath (Model.loadFromResources) and renders a complete DDL script for the session's backend: the object-sequence (if the backend supports sequences), every table-providing, non-provided entity'sCREATE TABLEand indexes, any schemas, and finally the foreign keys.runScript(Db, String)— executes that script against the low-level session.
Because the script is built from the same tentackle-model object graph the code generators use, the in-memory
test schema can never disagree with the generated persistence layer.
The Mock Delegates¶
The org.tentackle.pdo.mock package lets a unit test work with PDOs and operations without any persistence
backend. Recall the PDO pattern: a PDO is a dynamic proxy backed
by a domain delegate and a persistence delegate. The mocks supply trivial stand-ins for those delegates:
Where they live. The package is its own artifact,
tentackle-pdo-mock, which depends on nothing buttentackle-pdo. It sits below the domain- and persistence layer, so those modules can use the mocks in their own unit tests — a dependency ontentackle-test-pdowould be a cycle, because this module depends on them. Downstream projects need not care:tentackle-test-pdodepends ontentackle-pdo-mock, so the classes arrive transitively under their unchanged package and class names. See Tentackle PDO Mocks for the full description of the mocks — what they implement, how to write and generate one, and the Mockito alternative.
| Mock | Stands in for |
|---|---|
MockDomainObject<T,D> |
the domain delegate of a PersistentDomainObject |
MockPersistentObject<T,P> |
the persistence delegate of a PersistentDomainObject (also a PdoMethodCacheProvider) |
MockDomainOperation<T,D> |
the domain delegate of an Operation |
MockPersistentOperation<T> |
the persistence delegate of an Operation (also an OperationMethodCacheProvider) |
Most methods throw "not implemented" — the mocks deliberately implement only what a no-database test touches (id, session/context wiring, method caches), so you can exercise domain logic in isolation and get a loud failure the moment a test reaches for real persistence.
Build-Time SQL Generation¶
Besides shipping Java, this module's POM generates the framework's own schema and packages it as resources so downstream tests (and this module's tests) can stand up the TT tables in memory. The relevant build steps:
tentackle-maven-plugin(analyze,test-analyze) — the standard resource-index / analysis step.tentackle-sql-maven-plugin(create, ingenerate-resources) — runs over the framework model on the classpath and writes intotarget/generated-resources/META-INF/:sql/createttmodel.sql— the DDL for all framework tables, for all backends (backendNames=all), prepended withsrc/sql/ttheader.sql;model/…+MODEL-INDEX.LIST— a dump of the model source soModel.loadFromResources(...)can reload it from the jar;- an
object_sequence_idsequence definition (the PDO id generator).
These land under META-INF, are declared as a <resource> directory, and are therefore baked into the jar.
- maven-jar-plugin — sets Automatic-Module-Name: org.tentackle.pdo.test.
- maven-surefire-plugin — overridden to use the TestNG provider and the src/test/testng.xml suite.
Why the framework dependencies are optional¶
tentackle-persistence and tentackle-domain are declared optional with the comment "don't mix up classpath
order". The PDO runtime resolves the persistence and domain implementation layers via the
ServiceFinder SPI, and the order in which those layers
appear on the classpath matters. Marking them optional keeps this test artifact from forcing them transitively into
a downstream project's classpath in the wrong position; the downstream project declares the layers it actually
wants itself.
The Module's Own Tests¶
Although primarily a support library, the module also contains PDO tests (under src/test/java) that exercise
the framework end-to-end against the in-memory H2 database — including the multi-process ones, whose database
lives inside the forked server. They double as worked examples of the base classes above. Coverage spans, among
others:
| Area | Example test(s) |
|---|---|
| PDO factory & proxy wiring | PdoFactoryTest, PdoInvocationTest, PdoUtilitiesTest, IllegalModelNamesTest |
| Snapshots | SnapshotTest (see snapshot.md) |
| Model | ModelTest |
| Database / DBMS | BatchingTest, ResultSetTest, H2ConnectionTest, SqlScriptTest |
| Sessions & remoting | SessionPoolExecutorTest, RemoteTransactionTest, plus the out-of-JVM server in persist/server |
| Security & number sources | SecurityTest, NumberSourceTest |
| Preferences | RemotePreferencesTest |
The pdofactory test package (Invoice/SingleInvoice/CollectiveInvoice with handwritten domain and
persistence impls, an Operation and a LogIt interceptor) is a self-contained miniature PDO model used to test
the factory and interceptor machinery directly, without
running the code generator.
The testng.xml suite runs three groups — local, remote1, remote2 — single-threaded, over the whole
org.tentackle.* package tree; the remote* groups drive the out-of-process server. A fourth group,
postgres, is excluded by default and added by testng-postgres.xml under mvn -Ppostgres test.
A test method without a group never runs. Because the suite selects methods by group, a plain
@Testis silently skipped. Annotate new tests withgroups = "local"unless they belong in one of the others.
Test configuration¶
backend.properties — the file every test picks up by default — selects the in-memory H2 database
(jdbc:h2:mem:default;DB_CLOSE_DELAY=-1), so the suite is self-contained and needs no database setup.
DB_CLOSE_DELAY=-1 keeps the database alive while no connection is held, which matters for the pool tests.
client.properties holds the TRIP URL of the forked server (${dbService}), and backend.properties also
carries the matching service= entry the server listens on.
The forked server JVMs get their own empty in-memory database, so TestServer and AddAndRemovePrefsInServer
build the schema themselves in finishStartup() via InMemoryServerSchema — the counterpart of what
AbstractPdoTest does for the test JVM.
postgres.properties (filtered from dbUrl/dbUser/dbPasswd) points at a real PostgreSQL server. Only
PostgresScriptRunnerTest uses it: cancelling a running script needs a statement that blocks (PG_SLEEP) and
asserts PostgreSQL's own abort message, and it is the only coverage of PostgresScriptRunner. It belongs to the
postgres group, which testng.xml excludes; run it with mvn -Ppostgres test, which switches the suite to
testng-postgres.xml.
Logging is routed to target/logs/test.log via
tentackle-log-log4j2v because the
console is contended by parallel processes on Windows.
Using It in a Downstream Project¶
<dependency>
<groupId>org.tentackle</groupId>
<artifactId>tentackle-test-pdo</artifactId>
<version>${tentackle.version}</version>
<scope>test</scope>
</dependency>
A typical TestNG test then looks like:
public class CustomerTest extends org.tentackle.pdo.testng.AbstractPdoTest {
// default: one rolled-back transaction per class against the in-memory DB
public CustomerTest() {
super();
}
@Override
protected void populateDatabase() {
// insert fixtures once, after createDatabaseTables() built the schema
}
@Test(groups = "local")
public void createAndReload() {
Customer c = on(Customer.class);
c.setName("Acme");
c.save();
Assert.assertNotNull(c.getId());
// rolled back automatically at end of class
}
}
Override createDatabaseTables(Db) to add your application's model on top of the framework's TT tables, and choose
TransactionType.METHOD (or commit = true) where the default per-class rollback doesn't fit.
Generating Fixture Code with the PDO Browser¶
Writing the fixture code by hand gets tedious once tests need realistic object graphs. The PDO Browser — the
browse goal of the
wizard maven plugin —
automates it: it connects to a live backend (typically a development or staging database), lets you navigate the
persisted PDOs along their relations, and turns selected records into ready-to-paste Java code that recreates
them. The recipe:
- Start the PDO browser from the aggregator directory:
mvn tentackle-wizard:browse(the backendurlanduserare either configured in the POM or passed via-D). - Select the PDO type in the entity combo box and load records via load, find by ID, or use finder.
- In the table, select the PDOs to generate Java code for.
- Click generate — the browser builds the Java code (
on(...), the attribute setters, the component relations, a finalsave()) for the selected objects and copies it to the clipboard. - Paste the code into the test class (see
createDatabaseTables(Db)above): thepopulateDatabase()override is invoked right aftercreateDatabaseTables(Db)has built the in-memory schema, so the pasted code runs against a fresh database. - Repeat for all PDOs that should be available to all tests — put those into a common base test class of the suite — and then for the PDOs needed by the individual test classes.
Because the generated code recreates the objects through the plain PDO API, the tests need no backend at run time — the fixtures populate the in-memory H2 database like any other test code. This makes creating CI tests with realistic data super easy: browse, select, generate, paste.
Related Documentation¶
- PDO — Persistent Domain Objects — the runtime pattern under test.
- Operations — what the
Mock*Operationdelegates stand in for. - Snapshots — exercised by
SnapshotTest. - Tentackle Model — the object graph
DbTestUtilitiesturns into DDL. - Tentackle SQL —
Backend/DataTypeused to render the schema. - SQL Maven Plugin — generates the packaged TT DDL.
- Wizard Maven Plugin — the PDO Browser that generates test-fixture code.
- Session — sessions and the modification tracker the base classes drive.
- tentackle-test-fx-rdc — the analogous test-support module for the RDC/UI layer.