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,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