instancio: Instancio 6.1 lab (selectors, settings, seeds, records/sealed/generics, JUnit 6 extension, failing-seed capture)

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_017ic4b4VQVEJWetTT1dYzyJ
This commit is contained in:
2026-10-29 05:51:26 +00:00
co-authored by Claude Sonnet 5.5
parent 9d89323864
commit 11782a7ec3
39 changed files with 1053 additions and 0 deletions
+1
View File
@@ -7,6 +7,7 @@ Runnable companion code for the testing posts on [ankurm.com](https://ankurm.com
| [`mockito/`](mockito) | Mockito 5 Deep Dive: Static, Final and Constructor Mocking, ArgumentCaptor and Strict Stubs | Mockito 5.24.0, JUnit 6.1.3, JDK 25 | | [`mockito/`](mockito) | Mockito 5 Deep Dive: Static, Final and Constructor Mocking, ArgumentCaptor and Strict Stubs | Mockito 5.24.0, JUnit 6.1.3, JDK 25 |
| [`junit-6-migration/`](junit-6-migration) | JUnit 5 to 6 Migration: What Was Removed, What Changed and How to Upgrade | JUnit 5.14.4 and 6.1.3, JDK 25 | | [`junit-6-migration/`](junit-6-migration) | JUnit 5 to 6 Migration: What Was Removed, What Changed and How to Upgrade | JUnit 5.14.4 and 6.1.3, JDK 25 |
| [`assertj/`](assertj) | AssertJ Complete Guide: Soft Assertions, Recursive Comparison and Custom Assertions | AssertJ 3.27.7, JUnit 6.1.3, JDK 25 | | [`assertj/`](assertj) | AssertJ Complete Guide: Soft Assertions, Recursive Comparison and Custom Assertions | AssertJ 3.27.7, JUnit 6.1.3, JDK 25 |
| [`instancio/`](instancio) | Instancio: Generate Realistic Test Data Automatically in Java | Instancio 6.1.0, JUnit 6.1.3, JDK 25 |
Every module keeps the real output of its tests in `output/`. Each test writes the transcript and asserts the same facts, so the numbers in the posts fail the build if they stop being true. Every module keeps the real output of its tests in `output/`. Each test writes the transcript and asserts the same facts, so the numbers in the posts fail the build if they stop being true.
+1
View File
@@ -0,0 +1 @@
target/
+27
View File
@@ -0,0 +1,27 @@
# instancio
Companion code for the ankurm.com post *Instancio: Generate Realistic Test Data Automatically in Java* (https://ankurm.com/instancio-java-generate-test-data/).
| Component | Version |
|---|---|
| instancio-junit / instancio-core | 6.1.0 (GA; `6.0.0-RC1..RC4` exist on Maven Central but are release candidates) |
| JUnit Jupiter | 6.1.3 (the version `instancio-junit` 6.1.0 itself is built against) |
| slf4j-simple | 2.0.19, only in the optional `seed-log` profile |
| JDK | 25 (Temurin 25.0.5+8) |
```bash
./scripts/run-all.sh # tests + failure captures; regenerates output/*.txt
```
Each test writes a transcript to `output/` and asserts the same facts. Not stable between runs: `01`, `03` (random sizes), `15` (random ages), and the seeds in `17` and `18`.
| Test | Output | Topic |
|---|---|---|
| `BasicsTest` | 01-03 | `create`, what the defaults look like, other entry points |
| `CustomiseTest` | 04-08 | `set`, `generate`, `supply`, `ignore`, `withNullable`, `subtype`, selectors, unused-selector error |
| `SettingsAndSeedTest` | 09-11 | `Settings` keys, same seed gives equal objects |
| `GenericsAndSealedTest` | 12-14 | sealed hierarchy, `TypeToken`, record compact constructor |
| `JUnitExtensionTest` | 15 | `InstancioExtension`, `@WithSettings`, `@Given`, `@Seed`, parameterized tests |
| `FailingSeedDemoTest`, `ReproduceFailureDemoTest` | 16-18 | fail on purpose (only with `-Dlab.demo=...`), seed summary, reproduction |
`@InstancioSource` no longer exists in instancio-junit 6.x; the module uses `@Given` and `@MethodSource` instead.
+7
View File
@@ -0,0 +1,7 @@
// Instancio.create(Person.class) -- no seed pinned, so values differ on every run
id = <uuid> (non-null: true)
name = <4 chars>
email = <10 chars> (a random string, NOT an e-mail address)
homeAddress non-null: true
billingAddress non-null: true
orders.size() = 3 (default collection size is 2..6)
@@ -0,0 +1,7 @@
// 200 people from Instancio.of(Person.class).withSeed(1L).stream()
age range : 2 .. 9919 (0 negative)
name length range : 3 .. 10
emails containing '@': 0 of 200
orders per person : 2 .. 6
sample name : EOAPEZ
sample email : DZSPMQC
+7
View File
@@ -0,0 +1,7 @@
Instancio.create(String.class) -> 4 chars
Instancio.ofList(Person.class).size(3) -> list of 3
Instancio.createSet(Integer.class) -> set of 2
Instancio.ofMap(String, Integer).size(2) -> map of 2
Instancio.createBlank(Person.class) -> id=null name=null age=0 orders=[]
collection size pinned to 0 via Settings -> orders=[]
distinct names among the 3: 3
+6
View File
@@ -0,0 +1,6 @@
name = Asha Kulkarni
email matches e-mail regex = true
age in [18, 65] = true
homeAddress.postalCode matches \d{6} = true
homeAddress.country = IN
billingAddress.country = IN // one selector, two matching fields
@@ -0,0 +1,4 @@
supply(name, () -> "customer-" + n) -> customer-1
ignore(billingAddress) -> null
ignore(age) (an int) -> 0
withNullable(homeAddress), 200 people, seed 7 -> 40 null, 160 non-null
+3
View File
@@ -0,0 +1,3 @@
set(all(Address.class), pune) -> home=Pune, billing=Pune
set(field(Order::status), PAID) -> statuses of 5 orders: [PAID]
set(field(Order::total), 499.00) -> totals: [499.00]
+4
View File
@@ -0,0 +1,4 @@
subtype(payment, Card.class) -> Card
set(payment, new Cash()) -> Cash
card number length -> 16
card expiry is in the future -> true
+17
View File
@@ -0,0 +1,17 @@
field(Person.class, "doesNotExist"), first 8 lines:
org.instancio.exception.InstancioApiException:
Error creating an object
-> at lab.instancio.CustomiseTest.lambda$unusedSelectorsAreAnErrorByDefault$0(CustomiseTest.java:138)
Reason: invalid field 'doesNotExist' for class lab.instancio.domain.Person
set(field(Order::status), ..) on an Address graph, first 8 lines:
org.instancio.exception.UnusedSelectorException:
Found unused selectors referenced in the following methods:
-> Unused selector in: set()
1: field(Order::status)
at lab.instancio.CustomiseTest.lambda$unusedSelectorsAreAnErrorByDefault$1(CustomiseTest.java:139)
This error aims to highlight potential problems and help maintain clean test code.
with .lenient() the same call succeeds: true
+6
View File
@@ -0,0 +1,6 @@
// 100 people, Settings: collection 1..2, string length 8..12, lower-case alphabetic, int 18..80
orders per person : 1 .. 2
name length : 8 .. 12
age : 18 .. 80
all names lower-case: true
sample name : zopbsphrupid
@@ -0,0 +1,7 @@
create with seed 42, first call : LZORARZVM
create with seed 42, second call: LZORARZVM
a.equals(b) (whole object graph): true
a.hashCode() == b.hashCode() : true
create with seed 43 : UETEFLRO
a.equals(c) : false
seed 42 set through Keys.SEED : equals a = true
+5
View File
@@ -0,0 +1,5 @@
stream, seed 5, run 1: [QRUQXUOH, EPTMBHO, NSZCUSO, YROHHKRIGM, YXNEDMV]
stream, seed 5, run 2: [QRUQXUOH, EPTMBHO, NSZCUSO, YROHHKRIGM, YXNEDMV]
identical : true
5 creates without a seed -> 5 distinct names
seed 9 twice -> same orders list: true
+2
View File
@@ -0,0 +1,2 @@
// 300 x Instancio.of(PaymentMethod.class).withSeed(11L) (sealed: Card, BankTransfer, Cash)
subtypes seen: {BankTransfer=90, Card=105, Cash=105}
+14
View File
@@ -0,0 +1,14 @@
Instancio.of(new TypeToken<Page<Person>>() {}).size(items, 4)
items.size() = 4
items.get(0) is a Person = true
pageNumber in [1,10] = true
Instancio.create(new TypeToken<Map<String, List<Order>>>() {})
entries = 6, first value element is an Order = true
Instancio.create(Page.class) (raw class, T unknown), first 8 lines of the message:
Error creating an object
-> at lab.instancio.GenericsAndSealedTest.rawFailureMessage(GenericsAndSealedTest.java:99)
Reason: incorrect type parameters specified
-> Class lab.instancio.domain.Page requires 1 type parameter(s): [T]
-> The number of parameters provided was 0
+5
View File
@@ -0,0 +1,5 @@
record Percent(int value) { compact ctor rejects value < 0 or > 100 }
Instancio.of(Percent.class).withSeed(1L), 20 instances, default ints 1..10000:
no exception thrown; null elements: 20 of 20
with generate(all(int.class), ints().range(0, 100)):
null elements: 0 of 20, first values: [Percent[value=93], Percent[value=22], Percent[value=75]]
+9
View File
@@ -0,0 +1,9 @@
// instancio-junit 6.1.0 on JUnit Jupiter 6.1.3, lines collected inside the tests
@Given field and parameter: orders 3 and 2, ages in [18, 90]: true
@MethodSource Instancio.stream(): BWC with 3 orders
@MethodSource Instancio.stream(): PLDXIMRQFI with 4 orders
@MethodSource Instancio.stream(): ZQTUH with 4 orders
@ParameterizedTest [DE] gets its own Person, age 89
@ParameterizedTest [IN] gets its own Person, age 20
@ParameterizedTest [US] gets its own Person, age 75
@Seed(2026) on two different test methods + @Seed(2027) on a third: 3 Persons, 2 distinct
@@ -0,0 +1,14 @@
$ mvn test -Dtest=FailingSeedDemoTest -Dlab.demo=fail
[INFO] Running lab.instancio.FailingSeedDemoTest
SLF4J(W): No SLF4J providers were found.
SLF4J(W): Defaulting to no-operation (NOP) logger implementation
SLF4J(W): See https://www.slf4j.org/codes.html#noProviders for further details.
[ERROR] Tests run: 40, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 6.272 s <<< FAILURE! -- in lab.instancio.FailingSeedDemoTest
[ERROR] lab.instancio.FailingSeedDemoTest.everyAdultGetsADiscount(Person)[32] -- Time elapsed: 0.444 s <<< FAILURE!
org.opentest4j.AssertionFailedError: age 18 should get a discount ==> expected: <true> but was: <false>
[INFO] Results:
[ERROR] Failures:
[ERROR] FailingSeedDemoTest.everyAdultGetsADiscount:31 age 18 should get a discount ==> expected: <true> but was: <false>
[ERROR] Tests run: 40, Failures: 1, Errors: 0, Skipped: 0
lines containing '@Seed' in the whole Maven output: 0
@@ -0,0 +1,34 @@
$ mvn test -Pseed-log -Dtest=FailingSeedDemoTest -Dlab.demo=fail
[INFO] Running lab.instancio.FailingSeedDemoTest
[ERROR] Tests run: 40, Failures: 5, Errors: 0, Skipped: 0, Time elapsed: 4.204 s <<< FAILURE! -- in lab.instancio.FailingSeedDemoTest
[ERROR] lab.instancio.FailingSeedDemoTest.everyAdultGetsADiscount(Person)[4] -- Time elapsed: 0.128 s <<< FAILURE!
org.opentest4j.AssertionFailedError: age 18 should get a discount ==> expected: <true> but was: <false>
[ERROR] lab.instancio.FailingSeedDemoTest.everyAdultGetsADiscount(Person)[12] -- Time elapsed: 0.030 s <<< FAILURE!
org.opentest4j.AssertionFailedError: age 18 should get a discount ==> expected: <true> but was: <false>
[ERROR] lab.instancio.FailingSeedDemoTest.everyAdultGetsADiscount(Person)[24] -- Time elapsed: 0.011 s <<< FAILURE!
org.opentest4j.AssertionFailedError: age 18 should get a discount ==> expected: <true> but was: <false>
[ERROR] lab.instancio.FailingSeedDemoTest.everyAdultGetsADiscount(Person)[34] -- Time elapsed: 0.008 s <<< FAILURE!
org.opentest4j.AssertionFailedError: age 18 should get a discount ==> expected: <true> but was: <false>
[ERROR] lab.instancio.FailingSeedDemoTest.everyAdultGetsADiscount(Person)[37] -- Time elapsed: 0.007 s <<< FAILURE!
org.opentest4j.AssertionFailedError: age 18 should get a discount ==> expected: <true> but was: <false>
[main] WARN org.instancio.log.test.failure.seed - 5 tests failed. A failure can be reproduced by annotating the test method with @Seed:
@Seed(1592129627343010309L) FailingSeedDemoTest.everyAdultGetsADiscount repetition 4 of 40 (seed source: random seed)
@Seed(4014505636531349958L) FailingSeedDemoTest.everyAdultGetsADiscount repetition 12 of 40 (seed source: random seed)
@Seed(3480942169170837161L) FailingSeedDemoTest.everyAdultGetsADiscount repetition 24 of 40 (seed source: random seed)
@Seed(2463961442792581393L) FailingSeedDemoTest.everyAdultGetsADiscount repetition 34 of 40 (seed source: random seed)
@Seed(4455880775937018043L) FailingSeedDemoTest.everyAdultGetsADiscount repetition 37 of 40 (seed source: random seed)
[INFO] Results:
[ERROR] Failures:
[ERROR] FailingSeedDemoTest.everyAdultGetsADiscount:31 age 18 should get a discount ==> expected: <true> but was: <false>
[ERROR] FailingSeedDemoTest.everyAdultGetsADiscount:31 age 18 should get a discount ==> expected: <true> but was: <false>
[ERROR] FailingSeedDemoTest.everyAdultGetsADiscount:31 age 18 should get a discount ==> expected: <true> but was: <false>
[ERROR] FailingSeedDemoTest.everyAdultGetsADiscount:31 age 18 should get a discount ==> expected: <true> but was: <false>
[ERROR] FailingSeedDemoTest.everyAdultGetsADiscount:31 age 18 should get a discount ==> expected: <true> but was: <false>
[ERROR] Tests run: 40, Failures: 5, Errors: 0, Skipped: 0
@@ -0,0 +1,10 @@
$ mvn test -Pseed-log -Dtest=ReproduceFailureDemoTest -Dlab.demo=reproduce (run 1)
[ERROR] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 1.399 s <<< FAILURE! -- in lab.instancio.ReproduceFailureDemoTest
org.opentest4j.AssertionFailedError: age 18 should get a discount ==> expected: <true> but was: <false>
[main] WARN org.instancio.log.test.failure.seed - 1 test failed. A failure can be reproduced by annotating the test method with @Seed:
@Seed(2016013118612999037L) ReproduceFailureDemoTest.everyAdultGetsADiscount (seed source: @Seed)
$ same command again (run 2)
[ERROR] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 1.159 s <<< FAILURE! -- in lab.instancio.ReproduceFailureDemoTest
org.opentest4j.AssertionFailedError: age 18 should get a discount ==> expected: <true> but was: <false>
[main] WARN org.instancio.log.test.failure.seed - 1 test failed. A failure can be reproduced by annotating the test method with @Seed:
@Seed(2016013118612999037L) ReproduceFailureDemoTest.everyAdultGetsADiscount (seed source: @Seed)
+58
View File
@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.ankurm.testing</groupId>
<artifactId>instancio-lab</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.release>25</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<junit.version>6.1.3</junit.version>
<instancio.version>6.1.0</instancio.version>
<slf4j.version>2.0.19</slf4j.version>
</properties>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<!-- instancio-junit brings instancio-core transitively -->
<dependency>
<groupId>org.instancio</groupId>
<artifactId>instancio-junit</artifactId>
<version>${instancio.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<profiles>
<!-- Instancio logs the failing seed through SLF4J. With no SLF4J provider on the classpath that log
line is dropped, so activate this profile (-Pseed-log) to see it on the console. -->
<profile>
<id>seed-log</id>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>${slf4j.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</profile>
</profiles>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.6</version>
</plugin>
</plugins>
</build>
</project>
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
# Records what Instancio prints when a test fails, and how to reproduce it with @Seed.
# FailingSeedDemoTest fails on purpose, so every mvn call here is allowed to exit non-zero.
set -uo pipefail
cd "$(dirname "$0")/.."
clean() { grep -v -E '^(Picked up JAVA_TOOL_OPTIONS|WARNING:)' | grep -v -E '^\[INFO\] (Total time|Finished at)'; }
# 1. default classpath: no SLF4J provider, so the seed summary (logged via SLF4J) is dropped
RAW=$(mvn -B -T1 test -Dtest=FailingSeedDemoTest -Dlab.demo=fail 2>&1 | clean)
{
echo '$ mvn test -Dtest=FailingSeedDemoTest -Dlab.demo=fail'
echo "$RAW" | sed -n '/Running lab.instancio.FailingSeedDemoTest/,/Tests run: 40, Failures: [0-9]*, Errors: 0, Skipped: 0$/p' \
| grep -v -E '^\[INFO\] (BUILD|---|$)' | grep -v '^\s*at '
echo "lines containing '@Seed' in the whole Maven output: $(echo "$RAW" | grep -c '@Seed')"
} > output/16-failing-test-without-slf4j.txt
# 2. with an SLF4J provider (profile seed-log adds slf4j-simple) the summary appears
{
echo '$ mvn test -Pseed-log -Dtest=FailingSeedDemoTest -Dlab.demo=fail'
mvn -B -T1 test -Pseed-log -Dtest=FailingSeedDemoTest -Dlab.demo=fail 2>&1 | clean \
| sed -n '/Running lab.instancio.FailingSeedDemoTest/,/Tests run: 40, Failures: [0-9]*, Errors: 0, Skipped: 0$/p' \
| grep -v -E '^\[INFO\] (BUILD|---|$)' | grep -v '^\s*at '
} > output/17-failing-test-seed-summary.txt
# 3. the same assertion, pinned with a seed copied from a summary like the one above
{
echo '$ mvn test -Pseed-log -Dtest=ReproduceFailureDemoTest -Dlab.demo=reproduce (run 1)'
mvn -B -T1 test -Pseed-log -Dtest=ReproduceFailureDemoTest -Dlab.demo=reproduce 2>&1 | clean \
| grep -E 'Tests run:.*in lab|AssertionFailedError|@Seed' | head -4
echo '$ same command again (run 2)'
mvn -B -T1 test -Pseed-log -Dtest=ReproduceFailureDemoTest -Dlab.demo=reproduce 2>&1 | clean \
| grep -E 'Tests run:.*in lab|AssertionFailedError|@Seed' | head -4
} > output/18-reproduced-with-seed.txt
echo "captured: $(ls output | wc -l) files"
+8
View File
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
# Regenerates every file in instancio/output/ (tests write the transcripts and assert the same facts).
set -euo pipefail
cd "$(dirname "$0")/.."
rm -f output/*.txt
mvn -B -q clean test
./scripts/capture-failure.sh
echo "done: $(ls output | wc -l) files in output/"
@@ -0,0 +1,4 @@
package lab.instancio.domain;
public record Address(String street, String city, String postalCode, String country) {
}
@@ -0,0 +1,19 @@
package lab.instancio.domain;
/** Tiny code under test, used by the failing-seed demo. */
public final class DiscountService {
private DiscountService() {
}
/** Intended rule: 10 percent for ages 18 to 64, 20 percent for 65 and over, none under 18. */
public static int discountPercent(int age) {
if (age >= 65) {
return 20;
}
if (age > 18) { // off-by-one on purpose: age 18 gets nothing
return 10;
}
return 0;
}
}
@@ -0,0 +1,7 @@
package lab.instancio.domain;
import java.math.BigDecimal;
import java.util.UUID;
public record Order(UUID id, BigDecimal total, OrderStatus status, PaymentMethod payment) {
}
@@ -0,0 +1,3 @@
package lab.instancio.domain;
public enum OrderStatus { NEW, PAID, SHIPPED, CANCELLED }
@@ -0,0 +1,7 @@
package lab.instancio.domain;
import java.util.List;
/** A generic record: the type argument is erased at runtime, which is why TypeToken exists. */
public record Page<T>(List<T> items, int pageNumber, long totalItems) {
}
@@ -0,0 +1,16 @@
package lab.instancio.domain;
import java.time.YearMonth;
/** A sealed hierarchy: Instancio has to pick one of the permitted record subtypes. */
public sealed interface PaymentMethod {
record Card(String number, YearMonth expiry) implements PaymentMethod {
}
record BankTransfer(String iban) implements PaymentMethod {
}
record Cash() implements PaymentMethod {
}
}
@@ -0,0 +1,8 @@
package lab.instancio.domain;
import java.util.List;
import java.util.UUID;
public record Person(UUID id, String name, String email, int age,
Address homeAddress, Address billingAddress, List<Order> orders) {
}
@@ -0,0 +1,93 @@
package lab.instancio;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import lab.instancio.domain.Person;
import org.instancio.Instancio;
import org.instancio.Select;
import org.instancio.settings.Keys;
import org.instancio.settings.Settings;
import org.junit.jupiter.api.Test;
/** Output 01-03: the smallest thing that works, what "random" looks like, and how fields get values. */
class BasicsTest {
@Test
void createPopulatesTheWholeGraph() {
Person p = Instancio.create(Person.class);
assertNotNull(p.id());
assertNotNull(p.homeAddress());
assertNotNull(p.billingAddress());
assertTrue(p.orders().size() >= 2 && p.orders().size() <= 6, "default collection size is 2..6");
new Transcript("01-create-person.txt")
.line("// Instancio.create(Person.class) -- no seed pinned, so values differ on every run")
.line("id = <uuid> (non-null: %s)", p.id() != null)
.line("name = <%d chars>", p.name().length())
.line("email = <%d chars> (a random string, NOT an e-mail address)", p.email().length())
.line("homeAddress non-null: %s", p.homeAddress() != null)
.line("billingAddress non-null: %s", p.billingAddress() != null)
.line("orders.size() = %d (default collection size is 2..6)", p.orders().size())
.write();
}
@Test
void defaultsAreRandomNotRealistic() {
// Shape of the defaults, measured over 200 people with a fixed seed so the numbers are repeatable.
List<Person> people = Instancio.of(Person.class).withSeed(1L).stream().limit(200).toList();
int minAge = people.stream().mapToInt(Person::age).min().orElseThrow();
int maxAge = people.stream().mapToInt(Person::age).max().orElseThrow();
long negative = people.stream().filter(p -> p.age() < 0).count();
int minName = people.stream().mapToInt(p -> p.name().length()).min().orElseThrow();
int maxName = people.stream().mapToInt(p -> p.name().length()).max().orElseThrow();
long emailsWithAt = people.stream().filter(p -> p.email().contains("@")).count();
int minOrders = people.stream().mapToInt(p -> p.orders().size()).min().orElseThrow();
int maxOrders = people.stream().mapToInt(p -> p.orders().size()).max().orElseThrow();
assertEquals(0, emailsWithAt, "default Strings are not e-mail shaped");
new Transcript("02-defaults-over-200-people.txt")
.line("// 200 people from Instancio.of(Person.class).withSeed(1L).stream()")
.line("age range : %d .. %d (%d negative)", minAge, maxAge, negative)
.line("name length range : %d .. %d", minName, maxName)
.line("emails containing '@': %d of 200", emailsWithAt)
.line("orders per person : %d .. %d", minOrders, maxOrders)
.line("sample name : %s", people.get(0).name())
.line("sample email : %s", people.get(0).email())
.write();
}
@Test
void otherEntryPoints() {
String s = Instancio.create(String.class);
List<Person> three = Instancio.ofList(Person.class).size(3).create();
Set<Integer> ints = Instancio.createSet(Integer.class);
Map<String, Integer> map = Instancio.ofMap(String.class, Integer.class).size(2).create();
Person blank = Instancio.createBlank(Person.class);
Person lenient = Instancio.of(Person.class)
.withSettings(Settings.create().set(Keys.COLLECTION_MIN_SIZE, 0).set(Keys.COLLECTION_MAX_SIZE, 0))
.create();
assertEquals(3, three.size());
assertEquals(2, map.size());
new Transcript("03-entry-points.txt")
.line("Instancio.create(String.class) -> %d chars", s.length())
.line("Instancio.ofList(Person.class).size(3) -> list of %d", three.size())
.line("Instancio.createSet(Integer.class) -> set of %d", ints.size())
.line("Instancio.ofMap(String, Integer).size(2) -> map of %d", map.size())
.line("Instancio.createBlank(Person.class) -> id=%s name=%s age=%d orders=%s",
blank.id(), blank.name(), blank.age(), blank.orders())
.line("collection size pinned to 0 via Settings -> orders=%s", lenient.orders())
.line("distinct names among the 3: %d", three.stream().map(Person::name).collect(Collectors.toSet()).size())
.write();
}
}
@@ -0,0 +1,165 @@
package lab.instancio;
import static org.instancio.Select.all;
import static org.instancio.Select.field;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.math.BigDecimal;
import java.util.List;
import java.util.regex.Pattern;
import lab.instancio.domain.Address;
import lab.instancio.domain.Order;
import lab.instancio.domain.OrderStatus;
import lab.instancio.domain.PaymentMethod;
import lab.instancio.domain.Person;
import org.instancio.Instancio;
import org.junit.jupiter.api.Test;
/** Output 04-08: set, generate, supply, ignore, withNullable, subtype, and the selectors that target them. */
class CustomiseTest {
private static final Pattern EMAIL = Pattern.compile("^[^@\\s]+@[^@\\s]+\\.[a-z]+$");
@Test
void setAndGenerateAFewFields() {
Person p = Instancio.of(Person.class)
.set(field(Person::name), "Asha Kulkarni")
.generate(field(Person::email), gen -> gen.net().email())
.generate(field(Person::age), gen -> gen.ints().range(18, 65))
.generate(field(Address::postalCode), gen -> gen.string().digits().length(6))
.set(field(Address::country), "IN")
.create();
assertEquals("Asha Kulkarni", p.name());
assertTrue(EMAIL.matcher(p.email()).matches(), p.email());
assertTrue(p.age() >= 18 && p.age() <= 65);
assertTrue(p.homeAddress().postalCode().matches("\\d{6}"));
assertEquals("IN", p.billingAddress().country(), "field(Address::country) hits BOTH addresses");
new Transcript("04-set-and-generate.txt")
.line("name = %s", p.name())
.line("email matches e-mail regex = %s", EMAIL.matcher(p.email()).matches())
.line("age in [18, 65] = %s", p.age() >= 18 && p.age() <= 65)
.line("homeAddress.postalCode matches \\d{6} = %s", p.homeAddress().postalCode().matches("\\d{6}"))
.line("homeAddress.country = %s", p.homeAddress().country())
.line("billingAddress.country = %s // one selector, two matching fields", p.billingAddress().country())
.write();
}
@Test
void supplyIgnoreAndNullable() {
int[] counter = {0};
Person p = Instancio.of(Person.class)
.supply(field(Person::name), () -> "customer-" + (++counter[0]))
.ignore(field(Person::billingAddress))
.withNullable(field(Person::homeAddress))
.create();
assertEquals("customer-1", p.name());
assertNull(p.billingAddress(), "ignore() leaves the default: null for a reference type");
// ignore() on a primitive leaves 0, and ignore() on a record component can only produce the type's default.
Person noAge = Instancio.of(Person.class).ignore(field(Person::age)).create();
assertEquals(0, noAge.age());
// withNullable: across 200 people some homeAddress values are null and some are not (fixed seed, repeatable).
List<Person> people = Instancio.of(Person.class)
.withNullable(field(Person::homeAddress)).withSeed(7L).stream().limit(200).toList();
long nulls = people.stream().filter(x -> x.homeAddress() == null).count();
assertTrue(nulls > 0 && nulls < 200);
new Transcript("05-supply-ignore-nullable.txt")
.line("supply(name, () -> \"customer-\" + n) -> %s", p.name())
.line("ignore(billingAddress) -> %s", p.billingAddress())
.line("ignore(age) (an int) -> %d", noAge.age())
.line("withNullable(homeAddress), 200 people, seed 7 -> %d null, %d non-null", nulls, 200 - nulls)
.write();
}
@Test
void selectorsHitMoreThanOneField() {
// all(Address.class) targets every Address in the graph, including the ones inside Person.
Person p = Instancio.of(Person.class)
.set(all(Address.class), new Address("1 MG Road", "Pune", "411001", "IN"))
.set(field(Order::status), OrderStatus.PAID)
.set(field(Order::total), new BigDecimal("499.00"))
.create();
assertEquals("Pune", p.homeAddress().city());
assertEquals("Pune", p.billingAddress().city());
assertTrue(p.orders().stream().allMatch(o -> o.status() == OrderStatus.PAID));
assertTrue(p.orders().stream().allMatch(o -> o.total().compareTo(new BigDecimal("499.00")) == 0));
new Transcript("06-selectors.txt")
.line("set(all(Address.class), pune) -> home=%s, billing=%s",
p.homeAddress().city(), p.billingAddress().city())
.line("set(field(Order::status), PAID) -> statuses of %d orders: %s", p.orders().size(),
p.orders().stream().map(Order::status).distinct().toList())
.line("set(field(Order::total), 499.00) -> totals: %s",
p.orders().stream().map(Order::total).distinct().toList())
.write();
}
@Test
void subtypeForSealedAndInterfaceFields() {
Order o = Instancio.of(Order.class)
.subtype(field(Order::payment), PaymentMethod.Card.class)
.create();
assertTrue(o.payment() instanceof PaymentMethod.Card);
Order cash = Instancio.of(Order.class)
.set(field(Order::payment), new PaymentMethod.Cash())
.create();
// A card with a realistic expiry: drill into the record's own components.
Order card = Instancio.of(Order.class)
.subtype(field(Order::payment), PaymentMethod.Card.class)
.generate(field(PaymentMethod.Card::number), gen -> gen.string().digits().length(16))
.generate(field(PaymentMethod.Card::expiry), gen -> gen.temporal().yearMonth().future())
.create();
PaymentMethod.Card c = (PaymentMethod.Card) card.payment();
assertEquals(16, c.number().length());
assertNotNull(c.expiry());
new Transcript("07-subtype.txt")
.line("subtype(payment, Card.class) -> %s", o.payment().getClass().getSimpleName())
.line("set(payment, new Cash()) -> %s", cash.payment().getClass().getSimpleName())
.line("card number length -> %d", c.number().length())
.line("card expiry is in the future -> %s", c.expiry().isAfter(java.time.YearMonth.now()))
.write();
}
@Test
void unusedSelectorsAreAnErrorByDefault() {
// Strict mode: a selector that matches nothing is reported, which catches typos and stale tests.
String typo = failureOf(() -> Instancio.of(Person.class).ignore(field(Person.class, "doesNotExist")).create());
String unused = failureOf(() -> Instancio.of(Address.class).set(field(Order::status), OrderStatus.PAID).create());
assertTrue(unused.startsWith("org.instancio.exception.UnusedSelectorException"), unused);
// lenient() switches the check off for this one call
Address lenient = Instancio.of(Address.class).lenient().set(field(Order::status), OrderStatus.PAID).create();
assertNotNull(lenient);
new Transcript("08-unused-selector.txt")
.block("field(Person.class, \"doesNotExist\"), first 8 lines:", firstLines(typo, 8))
.block("set(field(Order::status), ..) on an Address graph, first 8 lines:", firstLines(unused, 8))
.line("with .lenient() the same call succeeds: %s", lenient != null)
.write();
}
private static String failureOf(Runnable r) {
try {
r.run();
return "no exception";
} catch (RuntimeException e) {
return e.getClass().getName() + ": " + e.getMessage();
}
}
private static String firstLines(String text, int n) {
return String.join("\n", java.util.Arrays.stream(text.split("\n")).limit(n).toList());
}
}
@@ -0,0 +1,34 @@
package lab.instancio;
import static org.junit.jupiter.api.Assertions.assertTrue;
import lab.instancio.domain.DiscountService;
import lab.instancio.domain.Person;
import org.instancio.junit.Given;
import org.instancio.junit.InstancioExtension;
import org.instancio.junit.WithSettings;
import org.instancio.settings.Keys;
import org.instancio.settings.Settings;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
import org.junit.jupiter.api.extension.ExtendWith;
/**
* Deliberately fails (DiscountService has an off-by-one for age 18), so it only runs when
* -Dlab.demo=fail is passed. scripts/capture-failure.sh runs it and records Instancio's real failure message.
*/
@EnabledIfSystemProperty(named = "lab.demo", matches = "fail")
@ExtendWith(InstancioExtension.class)
class FailingSeedDemoTest {
@WithSettings
private final Settings settings = Settings.create()
.set(Keys.INTEGER_MIN, 18)
.set(Keys.INTEGER_MAX, 30);
@RepeatedTest(40)
void everyAdultGetsADiscount(@Given Person adult) {
assertTrue(DiscountService.discountPercent(adult.age()) > 0,
"age " + adult.age() + " should get a discount");
}
}
@@ -0,0 +1,105 @@
package lab.instancio;
import static org.instancio.Select.all;
import static org.instancio.Select.field;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.function.Function;
import java.util.stream.Collectors;
import lab.instancio.domain.Order;
import lab.instancio.domain.Page;
import lab.instancio.domain.PaymentMethod;
import lab.instancio.domain.Person;
import org.instancio.Instancio;
import org.instancio.TypeToken;
import org.junit.jupiter.api.Test;
/** Output 12-14: records, sealed hierarchies and generic types. */
class GenericsAndSealedTest {
@Test
void sealedInterfaceYieldsEveryPermittedSubtype() {
Map<String, Long> counts = Instancio.of(PaymentMethod.class).withSeed(11L).stream().limit(300)
.collect(Collectors.groupingBy(p -> p.getClass().getSimpleName(), TreeMap::new, Collectors.counting()));
assertEquals(3, counts.size(), counts.toString());
assertTrue(counts.values().stream().allMatch(c -> c > 50), counts.toString());
new Transcript("12-sealed.txt")
.line("// 300 x Instancio.of(PaymentMethod.class).withSeed(11L) (sealed: Card, BankTransfer, Cash)")
.line("subtypes seen: %s", counts)
.write();
}
@Test
void genericRecordNeedsATypeToken() {
Page<Person> page = Instancio.of(new TypeToken<Page<Person>>() {})
.generate(field(Page.class, "pageNumber"), gen -> gen.ints().range(1, 10))
.size(field(Page.class, "items"), 4)
.create();
assertEquals(4, page.items().size());
assertTrue(page.items().get(0) instanceof Person);
Map<String, List<Order>> ordersByCustomer = Instancio.create(new TypeToken<Map<String, List<Order>>>() {});
assertFalse(ordersByCustomer.isEmpty());
Object firstValueElement = ordersByCustomer.values().iterator().next().get(0);
assertTrue(firstValueElement instanceof Order);
// Without a TypeToken the raw class loses T: Instancio.create(Page.class) cannot know the item type.
String rawMessage = rawFailureMessage();
new Transcript("13-type-token.txt")
.line("Instancio.of(new TypeToken<Page<Person>>() {}).size(items, 4)")
.line(" items.size() = %d", page.items().size())
.line(" items.get(0) is a Person = %s", page.items().get(0) instanceof Person)
.line(" pageNumber in [1,10] = %s", page.pageNumber() >= 1 && page.pageNumber() <= 10)
.line("Instancio.create(new TypeToken<Map<String, List<Order>>>() {})")
.line(" entries = %d, first value element is an Order = %s",
ordersByCustomer.size(), firstValueElement instanceof Order)
.block("Instancio.create(Page.class) (raw class, T unknown), first 8 lines of the message:", rawMessage)
.write();
}
@Test
void recordsAreBuiltThroughTheCanonicalConstructor() {
// Records have no setters and final fields; Instancio uses the canonical constructor, so
// a compact-constructor invariant is respected or reported.
record Percent(int value) {
Percent {
if (value < 0 || value > 100) {
throw new IllegalArgumentException("value out of range: " + value);
}
}
}
List<Percent> defaults = Instancio.of(Percent.class).withSeed(1L).stream().limit(20).toList();
long nulls = defaults.stream().filter(java.util.Objects::isNull).count();
List<Percent> withRange = Instancio.of(Percent.class)
.generate(all(int.class), gen -> gen.ints().range(0, 100)).stream().limit(20).toList();
long nullsWithRange = withRange.stream().filter(java.util.Objects::isNull).count();
assertEquals(0, nullsWithRange);
assertTrue(nulls > 0, "a constructor that throws does not fail the build: the element is null");
new Transcript("14-record-invariant.txt")
.line("record Percent(int value) { compact ctor rejects value < 0 or > 100 }")
.line("Instancio.of(Percent.class).withSeed(1L), 20 instances, default ints 1..10000:")
.line(" no exception thrown; null elements: %d of 20", nulls)
.line("with generate(all(int.class), ints().range(0, 100)):")
.line(" null elements: %d of 20, first values: %s", nullsWithRange, withRange.subList(0, 3))
.write();
}
private static String rawFailureMessage() {
try {
Instancio.create(Page.class);
return "no exception";
} catch (RuntimeException e) {
return String.join("\n", java.util.Arrays.stream(e.getMessage().strip().split("\n")).limit(8).toList());
}
}
}
@@ -0,0 +1,106 @@
package lab.instancio;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
import lab.instancio.domain.Person;
import org.instancio.Instancio;
import org.instancio.junit.Given;
import org.instancio.junit.InstancioExtension;
import org.instancio.junit.Seed;
import org.instancio.junit.WithSettings;
import org.instancio.settings.Keys;
import org.instancio.settings.Settings;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
/** Output 15-18: the JUnit 6 integration. Values come from the extension, settings from @WithSettings. */
@ExtendWith(InstancioExtension.class)
class JUnitExtensionTest {
// Applied to every Instancio call made by this test class (non-static is fine since 6.0.0).
@WithSettings
private final Settings settings = Settings.create()
.set(Keys.COLLECTION_MIN_SIZE, 1)
.set(Keys.COLLECTION_MAX_SIZE, 3)
.set(Keys.INTEGER_MIN, 18)
.set(Keys.INTEGER_MAX, 90);
@Given
private Person injectedField;
private static final List<String> LOG = new ArrayList<>();
@Test
void settingsAndFieldInjection(@Given Person param) {
assertTrue(injectedField.orders().size() <= 3, "@WithSettings applies to @Given values");
assertTrue(param.age() >= 18 && param.age() <= 90);
assertNotEquals(injectedField, param);
LOG.add("@Given field and parameter: orders " + injectedField.orders().size() + " and " + param.orders().size()
+ ", ages in [18, 90]: " + (param.age() >= 18 && param.age() <= 90));
}
private static final List<Person> SEEDED = new ArrayList<>();
@Seed(2026)
@Test
void seedAnnotationPinsTheDataA(@Given Person p) {
SEEDED.add(p);
}
@Seed(2026)
@Test
void seedAnnotationPinsTheDataB(@Given Person p) {
SEEDED.add(p);
}
@Seed(2027)
@Test
void otherSeed(@Given Person p) {
SEEDED.add(p);
}
@RepeatedTest(5)
void repeatedTestGetsFreshDataEachTime(@Given Person p) {
assertTrue(p.age() >= 18);
}
@ParameterizedTest
@ValueSource(strings = {"IN", "US", "DE"})
void valueSourcePlusGivenParameter(String country, @Given Person p) {
assertTrue(country.length() == 2);
assertTrue(p.age() >= 18);
LOG.add("@ParameterizedTest [" + country + "] gets its own Person, age " + p.age());
}
static Stream<Person> people() {
return Instancio.of(Person.class).withSeed(100L).stream().limit(3);
}
@ParameterizedTest
@MethodSource("people")
void methodSourceBackedByInstancioStream(Person p) {
assertTrue(p.orders().size() >= 2, "no @WithSettings in effect for a static source: default sizes 2..6");
LOG.add("@MethodSource Instancio.stream(): " + p.name() + " with " + p.orders().size() + " orders");
}
@org.junit.jupiter.api.AfterAll
static void writeTranscript() {
Transcript t = new Transcript("15-junit-extension.txt");
t.line("// instancio-junit 6.1.0 on JUnit Jupiter 6.1.3, lines collected inside the tests");
long distinct = SEEDED.stream().distinct().count();
assertEquals(2, distinct, "two methods with @Seed(2026) get the same Person; @Seed(2027) differs");
LOG.add("@Seed(2026) on two different test methods + @Seed(2027) on a third: " + SEEDED.size()
+ " Persons, " + distinct + " distinct");
LOG.stream().sorted().forEach(t::line);
t.write();
}
}
@@ -0,0 +1,36 @@
package lab.instancio;
import static org.junit.jupiter.api.Assertions.assertTrue;
import lab.instancio.domain.DiscountService;
import lab.instancio.domain.Person;
import org.instancio.junit.Given;
import org.instancio.junit.InstancioExtension;
import org.instancio.junit.Seed;
import org.instancio.junit.WithSettings;
import org.instancio.settings.Keys;
import org.instancio.settings.Settings;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
import org.junit.jupiter.api.extension.ExtendWith;
/**
* The same assertion as FailingSeedDemoTest, but as a plain @Test pinned with a seed copied from the
* failure summary in output/17-failing-test-seed-summary.txt. It fails every time, on the same Person.
*/
@EnabledIfSystemProperty(named = "lab.demo", matches = "reproduce")
@ExtendWith(InstancioExtension.class)
class ReproduceFailureDemoTest {
@WithSettings
private final Settings settings = Settings.create()
.set(Keys.INTEGER_MIN, 18)
.set(Keys.INTEGER_MAX, 30);
@Seed(2016013118612999037L)
@Test
void everyAdultGetsADiscount(@Given Person adult) {
assertTrue(DiscountService.discountPercent(adult.age()) > 0,
"age " + adult.age() + " should get a discount");
}
}
@@ -0,0 +1,103 @@
package lab.instancio;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.List;
import java.util.stream.IntStream;
import lab.instancio.domain.Person;
import org.instancio.Instancio;
import org.instancio.settings.Keys;
import org.instancio.settings.Settings;
import org.instancio.settings.StringCase;
import org.instancio.settings.StringType;
import org.junit.jupiter.api.Test;
/** Output 09-12: Settings (sizes, string lengths) and seeds (identical output, different output, streams). */
class SettingsAndSeedTest {
@Test
void settingsControlSizesAndStrings() {
Settings settings = Settings.create()
.set(Keys.COLLECTION_MIN_SIZE, 1)
.set(Keys.COLLECTION_MAX_SIZE, 2)
.set(Keys.STRING_MIN_LENGTH, 8)
.set(Keys.STRING_MAX_LENGTH, 12)
.set(Keys.STRING_TYPE, StringType.ALPHABETIC)
.set(Keys.STRING_CASE, StringCase.LOWER)
.set(Keys.INTEGER_MIN, 18)
.set(Keys.INTEGER_MAX, 80);
List<Person> people = Instancio.of(Person.class).withSettings(settings).withSeed(3L).stream().limit(100).toList();
int minOrders = people.stream().mapToInt(p -> p.orders().size()).min().orElseThrow();
int maxOrders = people.stream().mapToInt(p -> p.orders().size()).max().orElseThrow();
int minName = people.stream().mapToInt(p -> p.name().length()).min().orElseThrow();
int maxName = people.stream().mapToInt(p -> p.name().length()).max().orElseThrow();
int minAge = people.stream().mapToInt(Person::age).min().orElseThrow();
int maxAge = people.stream().mapToInt(Person::age).max().orElseThrow();
boolean allLower = people.stream().allMatch(p -> p.name().equals(p.name().toLowerCase()));
assertTrue(minOrders >= 1 && maxOrders <= 2);
assertTrue(minName >= 8 && maxName <= 12);
assertTrue(minAge >= 18 && maxAge <= 80);
assertTrue(allLower);
new Transcript("09-settings.txt")
.line("// 100 people, Settings: collection 1..2, string length 8..12, lower-case alphabetic, int 18..80")
.line("orders per person : %d .. %d", minOrders, maxOrders)
.line("name length : %d .. %d", minName, maxName)
.line("age : %d .. %d", minAge, maxAge)
.line("all names lower-case: %s", allLower)
.line("sample name : %s", people.get(0).name())
.write();
}
@Test
void sameSeedGivesIdenticalObjects() {
Person a = Instancio.of(Person.class).withSeed(42L).create();
Person b = Instancio.of(Person.class).withSeed(42L).create();
Person c = Instancio.of(Person.class).withSeed(43L).create();
assertEquals(a, b, "records compare component by component, so equals() proves identical content");
assertNotEquals(a, c);
// The seed can also live in Settings
Person d = Instancio.of(Person.class).withSettings(Settings.create().set(Keys.SEED, 42L)).create();
assertEquals(a, d);
new Transcript("10-seed-reproducibility.txt")
.line("create with seed 42, first call : %s", a.name())
.line("create with seed 42, second call: %s", b.name())
.line("a.equals(b) (whole object graph): %s", a.equals(b))
.line("a.hashCode() == b.hashCode() : %s", a.hashCode() == b.hashCode())
.line("create with seed 43 : %s", c.name())
.line("a.equals(c) : %s", a.equals(c))
.line("seed 42 set through Keys.SEED : equals a = %s", a.equals(d))
.write();
}
@Test
void streamsAreReproducibleAndNoSeedIsNot() {
List<String> first = Instancio.of(Person.class).withSeed(5L).stream().limit(5).map(Person::name).toList();
List<String> second = Instancio.of(Person.class).withSeed(5L).stream().limit(5).map(Person::name).toList();
assertEquals(first, second);
long distinctWithoutSeed = IntStream.range(0, 5)
.mapToObj(i -> Instancio.create(Person.class).name()).distinct().count();
assertEquals(5, distinctWithoutSeed);
// Instancio's own Random honours the seed too
Person p = Instancio.of(Person.class).withSeed(9L).create();
Person q = Instancio.of(Person.class).withSeed(9L).create();
new Transcript("11-seed-streams.txt")
.line("stream, seed 5, run 1: %s", first)
.line("stream, seed 5, run 2: %s", second)
.line("identical : %s", first.equals(second))
.line("5 creates without a seed -> %d distinct names", distinctWithoutSeed)
.line("seed 9 twice -> same orders list: %s", p.orders().equals(q.orders()))
.write();
}
}
@@ -0,0 +1,57 @@
package lab.instancio;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
/**
* Collects lines during a test and writes them to output/NN-name.txt.
* The same test asserts the same facts, so every figure in the blog post is an assertion.
*/
final class Transcript {
private final String file;
private final List<String> lines = new ArrayList<>();
Transcript(String file) {
this.file = file;
}
Transcript line(String format, Object... args) {
lines.add(args.length == 0 ? format : String.format(format, args));
return this;
}
/** A labelled multi-line message (an AssertionError message, usually), indented, unedited. */
Transcript block(String label, String text) {
lines.add(label);
for (String l : text.strip().split("\n")) {
lines.add(" " + l.stripTrailing());
}
return this;
}
/** Like block(), but drops stack-frame lines ("at ..." and "...(n remaining lines...)"). */
Transcript blockWithoutFrames(String label, String text) {
lines.add(label + " (stack frames omitted)");
for (String l : text.strip().split("\n")) {
if (l.stripLeading().startsWith("at ") || l.stripLeading().startsWith("...")) {
continue;
}
lines.add(" " + l.stripTrailing());
}
return this;
}
void write() {
try {
Path dir = Path.of("output");
Files.createDirectories(dir);
Files.writeString(dir.resolve(file), String.join("\n", lines) + "\n");
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}