1. Overview
Codion applications follow a layered testing approach, with each layer providing specific testing capabilities:
-
Domain Testing - Validates entity definitions, relationships, and domain logic
-
Model Testing - Tests business logic in edit and table models
-
Integration Testing - Verifies end-to-end functionality with database operations
3. Domain Testing
The DomainTest
base class provides comprehensive testing for entity definitions.
3.1. Basic Domain Test
public class ChinookTest extends DomainTest {
public ChinookTest() {
super(new ChinookImpl(), ChinookEntityFactory::new);
}
@Test
void album() {
test(Album.TYPE);
}
@Test
void artist() {
test(Artist.TYPE);
}
}
The test(EntityType)
method automatically verifies:
-
Entity can be instantiated
-
All attributes are correctly defined
-
Primary keys work correctly
-
Foreign key relationships are valid
-
Insert, update, delete, and select operations succeed
-
Entity validation rules are enforced
3.2. Custom Entity Factory
For entities with complex validation rules or required relationships, provide a custom EntityFactory:
public class ChinookEntityFactory extends DefaultEntityFactory {
@Override
public Entity entity(EntityType entityType) {
if (entityType.equals(InvoiceLine.TYPE)) {
return createInvoiceLine();
}
return super.entity(entityType);
}
private Entity createInvoiceLine() {
Entity invoiceLine = entities.entity(InvoiceLine.TYPE);
// Set required relationships and values
invoiceLine.set(InvoiceLine.INVOICE_FK, randomInvoice());
invoiceLine.set(InvoiceLine.TRACK_FK, randomTrack());
invoiceLine.set(InvoiceLine.QUANTITY, 1);
return invoiceLine;
}
}
3.3. Testing Domain Functions
Test database functions and procedures through the domain:
@Test
void randomPlaylist() {
EntityConnection connection = connection();
connection.startTransaction();
try {
List<Entity> genres = connection.select(Select.all(Genre.TYPE)
.limit(3)
.build());
Entity playlist = connection.execute(Playlist.RANDOM_PLAYLIST,
new RandomPlaylistParameters("Test Playlist", 10, genres));
assertNotNull(playlist);
assertEquals(10, connection.count(where(PlaylistTrack.PLAYLIST_FK.equalTo(playlist))));
}
finally {
connection.rollbackTransaction();
}
}
4. Model Testing
Test business logic in your Swing models independently of the UI.
4.1. Edit Model Testing
package is.codion.demos.world.model;
import is.codion.common.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().set(connectionProvider.connection().selectSingle(
Country.NAME.equalTo("Afghanistan")));
assertEquals(583_025, countryEditModel.averageCityPopulation().get());
countryEditModel.editor().defaults();
assertNull(countryEditModel.averageCityPopulation().get());
}
}
private static EntityConnectionProvider createConnectionProvider() {
return LocalEntityConnectionProvider.builder()
.domain(new WorldImpl())
.user(UNIT_TEST_USER)
.build();
}
}
4.2. Table Model Testing
Test table model behavior and master-detail relationships:
package is.codion.demos.chinook.model;
import is.codion.common.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.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() {
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.queryModel().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.detailModels().get(Track.TYPE).editModel().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();
}
}
5. Integration Testing
Test complete workflows across multiple entities and models.
5.1. Testing Report Generation
package is.codion.demos.world.model;
import is.codion.common.user.User;
import is.codion.common.value.Value;
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 is.codion.swing.common.model.worker.ProgressWorker.ProgressReporter;
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;
}
}
}
6. Test Utilities
6.1. Connection Provider Setup
The LocalEntityConnectionProvider.Builder
requires the codion.db.url
system property to be set:
-Dcodion.db.url=jdbc:h2:mem:h2db
Create connection providers for testing:
private static EntityConnectionProvider createConnectionProvider() {
return LocalEntityConnectionProvider.builder()
.domain(new WorldImpl())
.user(UNIT_TEST_USER)
.build();
}
Alternatively, you can provide your own Database
instance:
private static EntityConnectionProvider createConnectionProvider() {
Database database = H2DatabaseFactory.createDatabase("jdbc:h2:mem:testdb");
return LocalEntityConnectionProvider.builder()
.domain(new WorldImpl())
.database(database)
.user(UNIT_TEST_USER)
.build();
}
6.2. Transaction Management
Always use transactions for data modification tests:
@Test
void updateTest() {
EntityConnection connection = connection();
connection.startTransaction();
try {
// Perform updates
Entity entity = connection.selectSingle(Country.CODE2.equalTo("IS"));
entity.set(Country.POPULATION, 400_000);
connection.update(entity);
// Verify changes
Entity updated = connection.selectSingle(Country.CODE2.equalTo("IS"));
assertEquals(400_000, updated.get(Country.POPULATION));
}
finally {
connection.rollbackTransaction();
}
}
6.3. Test Data Builders
Create fluent builders for complex test data:
public class TestDataBuilder {
public static Entity.Builder customer() {
return entities.entity(Customer.TYPE)
.with(Customer.FIRST_NAME, "Test")
.with(Customer.LAST_NAME, "Customer")
.with(Customer.EMAIL, "test@example.com");
}
public static Entity.Builder invoice(Entity customer) {
return entities.entity(Invoice.TYPE)
.with(Invoice.CUSTOMER_FK, customer)
.with(Invoice.INVOICE_DATE, LocalDate.now())
.with(Invoice.TOTAL, BigDecimal.ZERO);
}
}
7. Testing Best Practices
-
Use DomainTest for all entity types - Even simple entities benefit from the comprehensive validation
-
Test with transactions - Always rollback to keep tests isolated
-
Test observable behavior - Verify that model state changes trigger appropriate notifications
-
Test validation rules - Ensure domain constraints are properly enforced
-
Test computed values - Verify derived attributes and denormalized values update correctly
-
Keep tests focused - Each test should verify one specific behavior
-
Use realistic test data - Your entity factories should create valid, meaningful entities