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

2. Test Configuration

2.1. User Configuration

The test user is configured via the codion.test.user system property:

./gradlew test -Dcodion.test.user=scott:tiger

Or in your test configuration:

private static final User UNIT_TEST_USER =
        User.parse(System.getProperty("codion.test.user", "scott:tiger"));

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

  1. Use DomainTest for all entity types - Even simple entities benefit from the comprehensive validation

  2. Test with transactions - Always rollback to keep tests isolated

  3. Test observable behavior - Verify that model state changes trigger appropriate notifications

  4. Test validation rules - Ensure domain constraints are properly enforced

  5. Test computed values - Verify derived attributes and denormalized values update correctly

  6. Keep tests focused - Each test should verify one specific behavior

  7. Use realistic test data - Your entity factories should create valid, meaningful entities