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:
@@ -0,0 +1 @@
|
||||
target/
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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]
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
// 300 x Instancio.of(PaymentMethod.class).withSeed(11L) (sealed: Card, BankTransfer, Cash)
|
||||
subtypes seen: {BankTransfer=90, Card=105, Cash=105}
|
||||
@@ -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
|
||||
@@ -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]]
|
||||
@@ -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)
|
||||
@@ -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>
|
||||
Executable
+34
@@ -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"
|
||||
Executable
+8
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user