Skip to main content

Testing with Random Values

If you enable enableTestInserts in typr.Options you get a TestInsert class with a method to insert a row for each table Typr knows about. All values except IDs and foreign keys are randomly generated, but you can override them.

Usage Example​

TestInsertExample
package showcase;

import java.math.BigDecimal;
import java.sql.Connection;
import java.util.Random;
import showcase.showcase.EmailAddress;

public class TestInsertExample {
public void example(Connection c) {
var testInsert = new TestInsert(new Random(0L), new DomainInsertImpl());

// Insert with just a name - everything else is random
var company1 = testInsert.showcaseCompany()
.withName("Acme Corp")
.insert(c);
// => CompanyRow[id=CCzLNHBFHuRvbI1iI19W, name=Acme Corp,
// foundedYear=Optional.empty, active=Optional[true],
// createdAt=Optional[2026-01-04T12:00:00]]

// Another company - all values random
var company2 = testInsert.showcaseCompany().insert(c);
// => CompanyRow[id=Hfyqts0coJXQqPyuxbr5, name=bFbQPNB7ZuKSWpBejTwv,
// foundedYear=Optional[1926371], active=Optional[true],
// createdAt=Optional[2026-01-04T12:00:00]]

// Department with FK to company - budget is random
var dept1 = testInsert.showcaseDepartment(company1.id())
.withName("Engineering")
.insert(c);
// => DepartmentRow[id=1NdpH7sjZu9W3pBe, companyId=CCzLNHBFHuRvbI1iI19W,
// name=Engineering, budget=Optional[8472.19]]

// Another department - all values random except FK
var dept2 = testInsert.showcaseDepartment(company1.id()).insert(c);
// => DepartmentRow[id=jTwvKSWpQqPyuxbr, companyId=CCzLNHBFHuRvbI1iI19W,
// name=xQqPyuxbr5Hfyqts, budget=Optional[3891.42]]

// Employee with FK to department - domains auto-generated
var employee = testInsert.showcaseEmployee(dept1.id())
.withFirstName("John")
.withLastName("Doe")
.insert(c);
// => EmployeeRow[id=9W3pBeKSWpQqPyux, departmentId=1NdpH7sjZu9W3pBe,
// email=EmailAddress[xK7mN2pQ@example.com], firstName=John, lastName=Doe,
// salary=Optional[PositiveAmount[4721.83]],
// phone=Optional[PhoneNumber[+1-555-847-2938]], ...]
}
}

You're setting up a graph of data in the database: Company β†’ Department β†’ Employee. The foreign keys force you to create data in the right order, and after each insert you get the persisted row back with its generated ID.

Almost no ceremony:

  • set only the values you care about
  • get random values for everything else
  • FKs are type-safe, so you can't mix them up
  • use a fixed seed for reproducible tests, or vary it to prove the random values don't matter

Generated TestInsert​

TestInsert
public record TestInsert(Random random, TestDomainInsert domainInsert) {
public TestInsert withRandom(Random random) {
return new TestInsert(random, domainInsert);
}

public TestInsert withDomainInsert(TestDomainInsert domainInsert) {
return new TestInsert(random, domainInsert);
}

public Inserter<AddressRowUnsaved, AddressRow> showcaseAddress(CustomerId customerId) {
return Inserter.of(
new AddressRowUnsaved(
new AddressId(RandomHelper.alphanumeric(random, 20)),
customerId,
Arrays.asList(AddressType.values())
.get(random.nextInt(Arrays.asList(AddressType.values()).size())),
RandomHelper.alphanumeric(random, 20),
RandomHelper.alphanumeric(random, 20),
(random.nextBoolean()
? Optional.empty()
: Optional.of(RandomHelper.alphanumeric(random, 20))),
(random.nextBoolean()
? Optional.empty()
: Optional.of(RandomHelper.alphanumeric(random, 20))),
RandomHelper.alphanumeric(random, 20),
(random.nextBoolean()
? Optional.empty()
: Optional.of(new PGpoint(random.nextDouble(), random.nextDouble()))),
(random.nextBoolean()
? Optional.empty()
: Optional.of(
new PGbox(
random.nextDouble(),
random.nextDouble(),
random.nextDouble(),
random.nextDouble()))),
(random.nextBoolean()
? Optional.empty()
: Optional.of(
new PGpolygon(
new PGpoint[] {
new PGpoint(0.0, 0.0),
new PGpoint(1.0, 0.0),
new PGpoint(1.0, 1.0),
new PGpoint(0.0, 1.0)
}))),
new UseDefault()),
(AddressRowUnsaved row, Connection c) -> (new AddressRepoImpl()).insert(row, c));
}

Domains​

If you use domains you typically want to control the generation of data yourself. For that reason there is an interface you need to implement and pass in. This only affects you if you use domains.

TestDomainInsert Interface​

TestDomainInsert
/** Methods to generate random data for domain types */
public interface TestDomainInsert {
/** Domain `showcase.email_address` Constraint: CHECK ((VALUE)::text ~~ '%@%.%'::text) */
EmailAddress showcaseEmailAddress(Random random);

/** Domain `showcase.percentage` Constraint: CHECK (VALUE >= 0 AND VALUE <= 100) */
Percentage showcasePercentage(Random random);

/** Domain `showcase.phone_number` Constraint: CHECK ((VALUE)::text ~ '^[+]?[0-9\-\s]+$'::text) */
PhoneNumber showcasePhoneNumber(Random random);

/** Domain `showcase.positive_amount` Constraint: CHECK (VALUE > 0) */
PositiveAmount showcasePositiveAmount(Random random);
}

DomainInsert Implementation​

DomainInsertImpl
package showcase;

import dev.typr.foundations.internal.RandomHelper;
import java.math.BigDecimal;
import java.util.Random;
import showcase.showcase.EmailAddress;
import showcase.showcase.Percentage;
import showcase.showcase.PhoneNumber;
import showcase.showcase.PositiveAmount;

public class DomainInsertImpl implements TestDomainInsert {
@Override
public EmailAddress showcaseEmailAddress(Random random) {
return new EmailAddress(RandomHelper.alphanumeric(random, 8) + "@example.com");
}

@Override
public PositiveAmount showcasePositiveAmount(Random random) {
return new PositiveAmount(BigDecimal.valueOf(Math.abs(random.nextDouble() * 10000) + 1));
}

@Override
public PhoneNumber showcasePhoneNumber(Random random) {
return new PhoneNumber("+1-555-" + random.nextInt(1000) + "-" + random.nextInt(10000));
}

@Override
public Percentage showcasePercentage(Random random) {
return new Percentage(BigDecimal.valueOf(random.nextDouble() * 100));
}
}

Comparison with ScalaCheck​

This does look a lot like ScalaCheck/property-based testing.

But look closer, there are:

  • no implicits or typeclasses to define
  • no integration glue code with test libraries
  • almost no imports needed, you mention very few types
  • no keeping track of all the possible row types and repositories
  • automatic handling of the FK graph

This feature is meant to be easy to use, and I really think/hope it is!