Database testing
The test-db module provides a JUnit 5 extension that backs Dari Record and Recordable classes with a real database during tests. Rather than mocking Brightspot's persistence layer, test-db starts an in-memory H2 database and registers it as the default database while a test class runs, so your test code exercises the same save, query, and validation paths that run in production. The underlying H2 instance itself persists across the whole test run rather than being recreated per class—see Sharing a database between tests below.
Do not use Mockito (or a similar mocking library) to mock Database, State, ObjectType, or other internal Dari/Brightspot classes. Do not mock static methods on Brightspot classes, such as Database.Static or Settings. These classes coordinate through static registries and settings-based context that a mock cannot faithfully reproduce, and mocking them hides real persistence bugs instead of catching them. Extend AbstractDatabaseTest instead, and let the framework provide a real backing database.
AbstractDatabaseTest
Extend AbstractDatabaseTest to run JUnit 5 tests against a real database. The following example constructs a Site configured with a primary URL—a site-level setting—saves an Article owned by that site, and queries the article back by ID.
1import com.psddev.cms.db.Site;2import com.psddev.dari.db.Query;3import com.psddev.test.db.AbstractDatabaseTest;4import org.junit.jupiter.api.AfterEach;5import org.junit.jupiter.api.Test;67import java.util.List;89import static org.assertj.core.api.Assertions.assertThat;1011public class ArticleTest extends AbstractDatabaseTest {1213private Site site;14private Article article;1516@AfterEach17void cleanUp() {18if (article != null) {19article.delete();20}21if (site != null) {22site.delete();23}24}2526@Test27void savesAndQueriesAnArticle() {28site = new Site();29site.setName("Test Site");30site.setUrls(List.of("https://example.com"));31site.save();3233article = new Article();34article.setHeadline("Astronaut escapes black hole");35article.as(Site.ObjectModification.class).setOwner(site);36article.save();3738Article found = Query.from(Article.class)39.where("_id = ?", article.getId())40.first();4142assertThat(found).isNotNull();43assertThat(found.getHeadline()).isEqualTo("Astronaut escapes black hole");44assertThat(found.as(Site.ObjectModification.class).getOwner().getPrimaryUrl()).isEqualTo("https://example.com");45}46}
The query is scoped to _id rather than a field like headline. The default test database is shared across every test class in the run (see Sharing a database between tests below), so a broader query could also match rows saved by other tests; querying by the object's own ID avoids that regardless of what else is in the database. The @AfterEach cleanup removes the Site and Article this test saves, for the reason described in Sharing a database between tests.
How the database behaves during a test
AbstractDatabaseTest is annotated with @ExtendWith(TestDatabaseExtension.class). Before any test method in the class runs, TestDatabaseExtension creates an H2 database, registers it as the default database by overriding Database#DEFAULT_DATABASE_SETTING, and refreshes DatabaseEnvironment so the current type definitions pick it up. After the class finishes, the extension clears the override.
Constructing a Record-extending object requires a resolvable default database, because the object's State binds to Database.Static#getDefault() as soon as it is created. This is why TestDatabaseExtension sets up the override in a beforeAll callback: without it, instantiating and saving any Record subclass in a test fails, since there is no database for the object to bind to.
The default database is a plain H2 relational engine with spatial indexing enabled, provisioned by DefaultTestDatabaseSupplier. It does not wire in Solr or any other search index. Predicates that depend on a full-text index, such as * matches * or _any matches someField, are not supported against the default test database and do not return meaningful results.
AbstractRecordTest
AbstractRecordTest<R extends Recordable> extends AbstractBeanTest<R>, adding bean property round-trip tests plus two equals/hashCode checks for a project type.
If the project uses Shared Tests, NoOverrideEqualsInRecordsTest and NoOverrideHashCodeInRecordsTest already enforce those same two rules across every Record class in the project automatically, and AbstractDatabaseTest already provides a real backing database. In that case, there is no reason to extend AbstractRecordTest or AbstractBeanTest—AbstractDatabaseTest is sufficient.
If your project is not on shared tests, or you still want AbstractRecordTest's bean property round-trip tests for a specific type, extend it directly:
1import com.psddev.test.db.AbstractRecordTest;23public class ArticleRecordTest extends AbstractRecordTest<Article> {4}
Extending AbstractRecordTest<Article> infers Article as the bean class under test through generics, and runs the following checks automatically:
- Bean property tests—inherited from
AbstractBeanTest, these introspect every declared property onArticleusing standard JavaBean conventions and generate a dynamic test per property, verifying that its getter and setter round-trip values correctly. noOverrideEquals—fails ifArticledeclares its ownequalsmethod.noOverrideHashCode—fails ifArticledeclares its ownhashCodemethod.
Dari relies on its own identity semantics for Record subclasses, so overriding equals or hashCode on a project type breaks assumptions the framework makes elsewhere.
Sharing a database between tests
By default, every test class that does not specify otherwise shares the same H2 database instance for the life of the test run. TestDatabaseExtension caches the Database in the JUnit 5 root extension context, keyed by the supplier class, so the same in-memory database persists across all test classes that use DefaultTestDatabaseSupplier—data saved in one test class is still there when the next test class runs.
To separate a test from the shared default database, implement Supplier<Database> and annotate the test method with @TestDatabase:
1import com.psddev.dari.db.Database;2import com.psddev.dari.h2.H2Database;3import com.psddev.test.db.AbstractDatabaseTest;4import com.psddev.test.db.TestDatabase;5import com.zaxxer.hikari.HikariDataSource;6import org.junit.jupiter.api.Test;78import java.util.function.Supplier;910public class IsolatedArticleTest extends AbstractDatabaseTest {1112@Test13@TestDatabase(IsolatedSupplier.class)14void runsAgainstItsOwnDatabase() {15// ...16}1718static class IsolatedSupplier implements Supplier<Database> {1920@Override21public Database get() {22HikariDataSource dataSource = new HikariDataSource();23dataSource.setJdbcUrl("jdbc:h2:mem:isolated;DB_CLOSE_DELAY=-1;OPTIMIZE_OR=false;OPTIMIZE_TWO_EQUALS=false");2425H2Database database = new H2Database();26database.setName("isolated");27database.setDataSource(dataSource);28database.setReadTimeout(10.0);29database.setIndexSpatial(true);30database.finishInitialization();3132return database;33}34}35}
IsolatedSupplier mirrors DefaultTestDatabaseSupplier's own setup, with a distinct database name so it does not collide with the default. Give each Supplier<Database> implementation its own name and JDBC URL—copying this example for a second isolated test without changing "isolated" points both at the same H2 instance instead of separate ones. @TestDatabase targets methods only—it cannot be applied at the class level.
The cache key is the supplier class, not the test method, so this only separates isolated tests from the shared default database—every test that uses the same Supplier<Database> implementation still shares that database with each other for the life of the run. If you do not isolate a test this way, clean up any objects your test saves—for example, in an @AfterEach method—so leftover data from one test does not affect another.
Constructing a Site with the same name or the same URL in two separate test classes causes a uniqueness constraint violation on the second save—both Site#name and Site#urls are annotated @Indexed(unique = true), and, without database isolation, both test classes save into the same underlying database. This includes the literal "https://example.com" used in the AbstractDatabaseTest example above: reusing that Site construction in a second test class without also carrying over its @AfterEach cleanup causes this collision. Use distinct values per test, clean up the Site when the test finishes (as shown in that example), or isolate the test with @TestDatabase, to avoid this.