Codion applications are tested in layers, matching how they are built:
-
Domain testing — entity definitions, relationships and database operations, via DomainTest
-
Model testing — application logic in edit and table models, headless
-
Integration testing — workflows across entities and models
Since the model layer has no UI dependency, everything below the panels is testable without showing a window.
1. Test configuration
The test user is configured via the codion.test.user system property:
./gradlew test -Dcodion.test.user=scott:tiger
2. Domain testing
The DomainTest base class verifies an entity definition against the database with a single method call per entity type: a randomly populated entity is inserted, selected, modified, updated and deleted, in a transaction which is rolled back — exercising the definition, its column mappings, primary key generation and foreign key relationships, without leaving a trace.
public class ChinookTest extends DomainTest {
public ChinookTest() {
super(new ChinookImpl(), ChinookEntityFactory::new);
}
@Test
void album() {
test(Album.TYPE);
}
@Test
void artist() {
test(Artist.TYPE);
}
}
2.1. Custom entity factory
The random entities are created by an EntityFactory — extend DefaultEntityFactory to control the values of specific attributes, for entities with constraints random values cannot satisfy:
private static final class ChinookEntityFactory extends DefaultEntityFactory {
private ChinookEntityFactory(EntityConnection connection) {
super(connection);
}
@Override
public void modify(Entity entity) {
super.modify(entity);
if (entity.type().equals(Album.TYPE)) {
entity.set(Album.TAGS, List.of("tag_one", "tag_two", "tag_three"));
}
}
@Override
protected <T> T value(Attribute<T> attribute) {
if (attribute.equals(Album.TAGS)) {
return (T) List.of("tag_one", "tag_two");
}
return super.value(attribute);
}
}
2.2. Testing database functions
Database functions and procedures are tested through a connection, in a transaction rolled back in a finally block — the standard pattern for any test modifying data:
@Test
void randomPlaylist() {
EntityConnection connection = connection();
connection.startTransaction();
try {
Entity genre = connection.selectSingle(Genre.NAME.equalTo("Metal"));
int noOfTracks = 10;
String playlistName = "MetalPlaylistTest";
RandomPlaylistParameters parameters = new RandomPlaylistParameters(playlistName, noOfTracks, List.of(genre));
Entity playlist = connection.execute(Playlist.RANDOM_PLAYLIST, parameters);
assertEquals(playlistName, playlist.get(Playlist.NAME));
List<Entity> playlistTracks = connection.select(PlaylistTrack.PLAYLIST_FK.equalTo(playlist));
assertEquals(noOfTracks, playlistTracks.size());
playlistTracks.stream()
.map(playlistTrack -> playlistTrack.get(PlaylistTrack.TRACK_FK))
.forEach(track -> assertEquals(genre, track.get(Track.GENRE_FK)));
}
finally {
connection.rollbackTransaction();
}
}
3. Model testing
Application logic in edit and table models is tested by constructing the model with a local connection provider and exercising it directly:
package is.codion.demos.world.model;
import is.codion.common.utilities.user.User;
import is.codion.demos.world.domain.WorldImpl;
import is.codion.demos.world.domain.api.World.Country;
import is.codion.framework.db.EntityConnectionProvider;
import is.codion.framework.db.local.LocalEntityConnectionProvider;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
public class CountryEditModelTest {
private static final User UNIT_TEST_USER =
User.parse(System.getProperty("codion.test.user", "scott:tiger"));
@Test
void averageCityPopulation() {
try (EntityConnectionProvider connectionProvider = createConnectionProvider()) {
CountryEditModel countryEditModel = new CountryEditModel(connectionProvider);
countryEditModel.editor().entity().set(connectionProvider.connection().selectSingle(
Country.NAME.equalTo("Afghanistan")));
assertEquals(583_025, countryEditModel.averageCityPopulation().get());
countryEditModel.editor().entity().defaults();
assertNull(countryEditModel.averageCityPopulation().get());
}
}
private static EntityConnectionProvider createConnectionProvider() {
return LocalEntityConnectionProvider.builder()
.domain(new WorldImpl())
.user(UNIT_TEST_USER)
.build();
}
}
Master-detail behavior tests the same way — select in the master, assert the detail:
package is.codion.demos.chinook.model;
import is.codion.common.utilities.user.User;
import is.codion.demos.chinook.domain.ChinookImpl;
import is.codion.demos.chinook.domain.api.Chinook.Album;
import is.codion.demos.chinook.domain.api.Chinook.Track;
import is.codion.framework.db.EntityConnection;
import is.codion.framework.db.EntityConnectionProvider;
import is.codion.framework.db.local.LocalEntityConnectionProvider;
import is.codion.framework.domain.entity.Entity;
import is.codion.framework.domain.entity.exception.EntityValidationException;
import is.codion.swing.framework.model.SwingEntityTableModel;
import org.junit.jupiter.api.Test;
import java.util.List;
import static is.codion.framework.db.EntityConnection.Update.where;
import static org.junit.jupiter.api.Assertions.assertEquals;
public final class AlbumModelTest {
private static final String MASTER_OF_PUPPETS = "Master Of Puppets";
@Test
void albumRefreshedWhenTrackRatingIsUpdated() throws EntityValidationException {
try (EntityConnectionProvider connectionProvider = createConnectionProvider()) {
EntityConnection connection = connectionProvider.connection();
connection.startTransaction();
// Initialize all the tracks with an inital rating of 8
Entity masterOfPuppets = connection.selectSingle(Album.TITLE.equalTo(MASTER_OF_PUPPETS));
connection.update(where(Track.ALBUM_FK.equalTo(masterOfPuppets))
.set(Track.RATING, 8)
.build());
// Re-select the album to get the updated rating, which is the average of the track ratings
masterOfPuppets = connection.selectSingle(Album.TITLE.equalTo(MASTER_OF_PUPPETS));
assertEquals(8, masterOfPuppets.get(Album.RATING));
// Create our AlbumModel and configure the query condition
// to populate it with only Master Of Puppets
AlbumModel albumModel = new AlbumModel(connectionProvider);
SwingEntityTableModel albumTableModel = albumModel.tableModel();
albumTableModel.query().condition().get(Album.TITLE).set().equalTo(MASTER_OF_PUPPETS);
albumTableModel.items().refresh();
assertEquals(1, albumTableModel.items().size());
List<Entity> modifiedTracks = connection.select(Track.ALBUM_FK.equalTo(masterOfPuppets)).stream()
.peek(track -> track.set(Track.RATING, 10))
.toList();
// Update the tracks using the edit model
albumModel.detail().get(Track.TYPE).editor().update(modifiedTracks);
// Which should trigger the refresh of the album in the Album model
// now with the new rating as the average of the track ratings
assertEquals(10, albumTableModel.items().included().get(0).get(Album.RATING));
connection.rollbackTransaction();
}
}
private static EntityConnectionProvider createConnectionProvider() {
return LocalEntityConnectionProvider.builder()
.domain(new ChinookImpl())
.user(User.parse("scott:tiger"))
.build();
}
}
4. Integration testing
Complete workflows, such as report generation, follow the same shape:
package is.codion.demos.world.model;
import is.codion.common.model.worker.ProgressWorker.ProgressReporter;
import is.codion.common.reactive.value.Value;
import is.codion.common.utilities.user.User;
import is.codion.demos.world.domain.WorldImpl;
import is.codion.demos.world.domain.api.World.City;
import is.codion.demos.world.domain.api.World.Country;
import is.codion.framework.db.EntityConnection;
import is.codion.framework.db.EntityConnectionProvider;
import is.codion.framework.db.local.LocalEntityConnectionProvider;
import is.codion.framework.domain.entity.Entity;
import is.codion.framework.domain.entity.attribute.Attribute;
import net.sf.jasperreports.engine.JRDataSource;
import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JRField;
import net.sf.jasperreports.engine.base.JRBaseField;
import org.junit.jupiter.api.Test;
import java.util.List;
import static is.codion.framework.db.EntityConnection.Select.where;
import static is.codion.framework.domain.entity.OrderBy.ascending;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
public final class CountryReportDataSourceTest {
private static final User UNIT_TEST_USER =
User.parse(System.getProperty("codion.test.user", "scott:tiger"));
@Test
void iterate() throws JRException {
try (EntityConnectionProvider connectionProvider = createConnectionProvider()) {
Value<Integer> progressCounter = Value.nullable();
Value<String> publishedValue = Value.nullable();
ProgressReporter<String> progressReporter = new ProgressReporter<>() {
@Override
public void report(int progress) {
progressCounter.set(progress);
}
@Override
public void publish(String... chunks) {
publishedValue.set(chunks[0]);
}
};
EntityConnection connection = connectionProvider.connection();
List<Entity> countries =
connection.select(where(Country.NAME.in("Denmark", "Iceland"))
.orderBy(ascending(Country.NAME))
.build());
CountryReportDataSource countryReportDataSource = new CountryReportDataSource(countries.iterator(), connection, progressReporter);
assertThrows(IllegalStateException.class, countryReportDataSource::cityDataSource);
countryReportDataSource.next();
assertEquals("Denmark", countryReportDataSource.getFieldValue(field(Country.NAME)));
assertEquals("Europe", countryReportDataSource.getFieldValue(field(Country.CONTINENT)));
assertEquals("Nordic Countries", countryReportDataSource.getFieldValue(field(Country.REGION)));
assertEquals(43094d, countryReportDataSource.getFieldValue(field(Country.SURFACEAREA)));
assertEquals(5330000, countryReportDataSource.getFieldValue(field(Country.POPULATION)));
assertThrows(JRException.class, () -> countryReportDataSource.getFieldValue(field(City.LOCATION)));
JRDataSource denmarkCityDataSource = countryReportDataSource.cityDataSource();
denmarkCityDataSource.next();
assertEquals("København", denmarkCityDataSource.getFieldValue(field(City.NAME)));
assertEquals(495699, denmarkCityDataSource.getFieldValue(field(City.POPULATION)));
assertThrows(JRException.class, () -> denmarkCityDataSource.getFieldValue(field(Country.REGION)));
denmarkCityDataSource.next();
assertEquals("Århus", denmarkCityDataSource.getFieldValue(field(City.NAME)));
countryReportDataSource.next();
assertEquals("Iceland", countryReportDataSource.getFieldValue(field(Country.NAME)));
JRDataSource icelandCityDataSource = countryReportDataSource.cityDataSource();
icelandCityDataSource.next();
assertEquals("Reykjavík", icelandCityDataSource.getFieldValue(field(City.NAME)));
assertEquals(2, progressCounter.get());
assertEquals("Iceland", publishedValue.get());
}
}
private static EntityConnectionProvider createConnectionProvider() {
return LocalEntityConnectionProvider.builder()
.domain(new WorldImpl())
.user(UNIT_TEST_USER)
.build();
}
private static JRField field(Attribute<?> attribute) {
return new TestField(attribute.name());
}
private static final class TestField extends JRBaseField {
private TestField(String name) {
this.name = name;
}
}
}
5. Practices
-
Run DomainTest over every entity type — it is one line per entity and catches definition/schema drift immediately.
-
Transactions with rollback keep tests isolated and repeatable — no test data cleanup.
-
Test observable behavior — model state is exposed as Value/State/Event; assert on those, the same surface the UI binds to.
-
Load test early — see application load testing for exercising the application under concurrent users.