Introduction¶
Overview¶
Instancio is a Java library for generating test objects. Its main goal is to reduce manual data setup in unit tests. Its API was designed to be as non-intrusive and as concise as possible, while providing enough flexibility to customise generated objects. Instancio requires no changes to production code, and it can be used out-of-the-box with zero configuration.
Project Goals¶
There are several existing libraries for generating realistic test data, such as addresses, first and last names, and so on. While Instancio also supports this use case, this is not its goal. The idea behind the project is that most unit tests do not care what the actual values are. They just require the presence of a value. Therefore, the main goal of Instancio is simply to generate fully populated objects with random data, including arrays, collections, nested collections, generic types, and so on. And it aims to do so with as little code as possible to keep the tests concise.
Another goal of Instancio is to make the tests more dynamic. Since each test run is against random values, the tests become alive. They cover a wider range of inputs, which might help uncover bugs that may have gone unnoticed with static data. In many cases, the random nature of the data also removes the need for parameterising test methods.
Finally, Instancio aims to provide reproducible data. It uses a consistent seed value for each object graph it generates. Therefore, if a test fails against a given set of inputs, Instancio supports re-generating the same data set in order to reproduce the failed test.
Instancio Basics¶
Creating Objects¶
The Instancio class is the entry point to the API. It provides the following shorthand methods for creating objects. These can be used when defaults suffice and generated values do not need to be customised.
Shorthand methods | |
---|---|
The following builder methods allow chaining additional method calls in order to customise generated values, ignore certain fields, provide custom settings, and so on.
Builder API | |
---|---|
The three arguments accepted by these methods can be used for different purposes.
It should be noted that generic types can also be created using Instancio.of(Class)
and specifying the type parameters as arguments to the withTypeParameters()
method:
However, this approach has a couple of drawbacks: it does not support nested generics, and its usage will produce an "unchecked assignment" warning.
Creating collections¶
Collections can be created using one of the following methods. These methods create a collection of random size, between 2 and 6 elements, inclusive:
Collections API: using Class | |
---|---|
In addition, there is a builder API for creating a collection of a specified size. The builder API also supports customising properties of collection elements.
Collections builder API: using Class | |
---|---|
If the element is a generic type, the following methods can be used instead:
Collections builder API: using TypeToken | |
---|---|
In addition, ofList()
and ofSet()
can be used to create collections from models:
Collections builder API: using Model | |
---|---|
Specifying the collection size is optional. If no size is specified, a collection of random size (between 2 and 6 inclusive) will be generated.
Creating record
and sealed
Classes¶
Instancio version 1.5.0
introduced support for creating
record
classes when run on Java 16+, andsealed
classes when run on Java 17+.
This uses the same API as described above for creating regular classes.
Creating a Stream of Objects¶
Instancio also provides methods for creating a Stream
of objects. The stream()
methods return an infinite stream of distinct fully-populated instances. Similarly to the create()
methods, these have a shorthand form if no customisations are needed:
as well as the builder API that allows customising generated values:
Stream Builder API | |
---|---|
The following are a couple of examples of using streams. Note the calls to limit()
, which are required to avoid an infinite loop.
Since returned streams are infinite, limit()
must be called to avoid an infinite loop.
Creating Blank Objects¶
Experimental API @since 4.7.0
In addition to creating fully-populated objects, Instancio provides an API for creating blank objects using the following methods:
Blank objects have value fields (such as strings, numbers, dates) set to null
and nested POJO references initialised to blank POJOs. The following is a simple example that assumes a Person
class with a name
and address
fields:
See the documentation for setBlank(TargetSelector)
method for more details on blank objects.
Creating Simple Values¶
Instancio offers the Instancio.gen()
method to generate simple value types like strings, numbers, and dates. This method serves as the entry point for accessing built-in generators. All generators available through Instancio.gen()
implement the ValueSpec interface, which provides the following methods:
Method | Description |
---|---|
get() | generates a single value |
list(int size) | generates a list of values of given size |
stream() | generates an infinite stream of values (requires calling limit() ) |
map(Function) | returns the value after applying the function |
nullable() | indicates that a null may be generated |
See Built-in Generators for a list of available generators
Selectors¶
Selectors are used to target fields and classes, for example in order to customise generated values. Instancio supports different types of selectors, all of which implement the TargetSelector interface. These types are:
All selectors can be created using static methods from the Select class.
The following examples assume import org.instancio.Select
Regular selectors¶
Regular selectors are for precise matching: they can only match a single field or a single type.
Select.field()
is matched based on the exact field name. If a field with the specified name does not exist, an error will be thrown. Select.all()
is matched using Class
equality, therefore matching does not include subtypes.
Method reference selector¶
This selector uses method references to match fields.
Internally, method reference is converted to a regular field selector, equivalent to Select.field(Class<?> declaringClass, String fieldName)
. This is done by mapping the method name to the corresponding field name. The mapping logic supports the following naming conventions:
- Java beans - where getters are prefixed with
get
and, in case of booleans,is
. - Java record - where method names match field names exactly.
For example, all the following combinations of field and method names are supported:
Method name | Field name | Example |
---|---|---|
getName() | name | Select.field(Person::getName) -> Select.field(Person.class, "name") |
name() | name | Select.field(Person::name) -> Select.field(Person.class, "name") |
isActive() | active | Select.field(Person::isActive) -> Select.field(Person.class, "active") |
isActive() | isActive | Select.field(Person::isActive) -> Select.field(Person.class, "isActive") |
For methods that follow other naming conventions, or situations where no method is available, regular field selectors can be used instead.
Regular selector definition
From here on, the definition of regular selectors also includes method reference selectors.
Kotlin method reference selector¶
Since Instancio does not include Kotlin dependencies, the method reference selector described above is only supported for Java classes. If you use Kotlin, a similar selector can be implemented as a simple utility class shown below. This sample assumes that the property has a non-null backing javaField
:
Sample implementation method reference selector for Kotlin | |
---|---|
Predicate selectors¶
Predicate selectors allow for greater flexibility in matching fields and classes. These use a plural naming convention: fields()
and types()
.
Select.fields(field -> field.getName().contains("date"))
Select.types(klass -> Collection.class.isAssignableFrom(klass))
Unlike regular selectors, these can match multiple fields or types. For example, they can be used to match all fields declared by a class, or all classes within a package. They can also be used to match a certain type, including its subtypes.
Convenience selectors¶
Convenience selectors provide syntactic sugar built on top of regular and predicate selectors.
Select.all(String.class)
.
Select.all(Select.all(int.class), Select.all(Integer.class))
.
Predicate<Field>
selectors.
Predicate<Class<?>>
selectors.
The Select.allXxx()
methods such as Select.allInts()
, are available for all core types.
Select.all(GroupableSelector... selectors)
This method can be used for grouping multiple selectors, allowing for more concise code as shown below.
Select.all(
Select.field(Address::getCity),
Select.field(Address.class, "postalCode"),
Select.all(Phone.class))
Select.fields()
andSelect.types()
These selectors provide a builder API for constructing predicate selectors. For example, the following selector matches Long
fields annotated with @Id
which is equivalent to using the following predicate:
Select.root()
This method selects the root object. The following snippet creates nested lists, where the outer list and inner lists have different sizes:
In this case, Select.all(List.class)
matches all lists except the outer list, because Select.root()
selector has higher precedence than other selectors.
Setter selectors¶
Setter selectors are available since version 4.0.0
Setter selectors allow targeting setter methods. These selectors can only be used if Method Assignment is enabled.
Parameter type must be specified for overloaded methods.
Omitting the parameter type will lead to undefined behaviour.
Select.setter(Person.class, "setName")
Select.setter(Pojo.class, "setValue", String.class) // Pojo.setValue(String)
Select.setter(Pojo.class, "setValue", Long.class) // Pojo.setValue(Long)
In addition, a setter can be selected using the method reference selector:
Selector Precedence¶
Selector precedence rules apply when multiple selectors match a field or class:
- Regular selectors have higher precedence than predicate selectors.
- Field selectors have higher precedence than type selectors.
Consider the following example:
This will produce an address object with all strings set to "foo". However, since field selectors have higher precedence, the city will be set to "bar".
In the following example, the city will also be set to "bar" because the predicate Select.fields()
selector has lower precedence than the regular Select.field()
selector:
Multiple matching selectors¶
When more than one selector matches a given target, then the last selector wins. This rule applies to both, regular and predicate selectors.
In the case of regular selectors, if the two are identical, the last selector simply replaces the first (internally, regular selectors are stored as Map
keys).
Predicate selectors, on the other hand, are stored as a List
and evaluated sequentially starting from the last entry.
In this particular example, the first entry remains unused, which would cause an unused selector error. Therefore lenient()
mode must be enabled to prevent the error (see Selector Strictness).
Selector Scopes¶
Selectors provide the within(Scope... scopes)
method for fine-tuning the targets selectors should be applied to. Instancio supports two types of scope:
- Class-level scope: narrows down a selector to the specified class.
- Field-level scope: narrows down a selector to the specified field of the target class.
- Predicate scope: narrows down a selector using a field or class predicate.
To illustrate how scopes work we will assume the following structure for the Person
class (getters and setters omitted).
class Person {
String name;
Address homeAddress;
Address workAddress;
}
class Address {
String street;
String city;
List<Phone> phoneNumbers;
}
class Phone {
String areaCode;
String number;
}
Note that the Person
class has two Address
fields. Selecting field(Address::getCity)
would target both addresses, homeAddress
and workAddress
. Without scopes, it would not be possible to set the two cities to different values. Using scopes solves this problem:
For more complex class structures, multiple scopes can be specified using within(Scope...)
method. When specifying multiple scopes, the order is important: from outermost to innermost scope. Additional examples are provided below.
Creating scopes¶
Scopes can be created using:
Select.scope()
static methods in the Select classSelector.toScope()
method provided by regular selectorsPredicateSelector.toScope()
method provided by predicate selectors
The first approach requires specifying the target class and, for field-level scopes, the name of the field:
Select.scope(Phone.class);
Select.scope(Person.class, "homeAddress");
Select.scope(Person::getHomeAddress);
Select.scope(type -> type == Address.class);
The second approach is to create scopes from selectors using the toScope()
method.
Select.all(Phone.class).toScope();
Select.field(Person.class, "homeAddress").toScope();
Select.field(Person::getHomeAddress).toScope();
Select.fields(field -> field.getName().equals("id")).toScope();
Select.types(type -> type == Address.class).toScope();
Examples of using scopes¶
To start off, without using scopes we can set all strings to the same value. For example, the following snippet will set each string field of each class to "foo".
Set all strings to "Foo" | |
---|---|
Using within()
we can narrow down the scope of the allStrings()
selector as shown in the following examples. For brevity, Instancio.of(Person.class)
is omitted.
Select.allStrings().within(scope(Address.class))
Select.allStrings().within(scope(List.class))
Select.allStrings().within(scope(Person.class, "homeAddress"))
Using within()
also allows specifying multiple scopes. Scopes must be specified top-down, starting from the outermost to the innermost.
Select.allStrings().within(scope(Person::getWorkAddress), scope(Phone.class))
The Person.workAddress
object contains a list of phones, therefore Person.workAddress
is the outermost scope and is specified first. Phone
class is the innermost scope and is specified last.
The final examples illustrate the creation of scope objects from regular selectors. The following examples are equivalent to each other:
Select.allStrings().within(scope(Person.class, "homeAddress"))
Select.allStrings().within(scope(Person::getHomeAddress))
Select.allStrings().within(field(Person.class, "homeAddress").toScope())
Select.allStrings().within(field(Person::getHomeAddress).toScope())
Select.allStrings().within(scope(Person.class))
Select.allStrings().within(all(Person.class).toScope())
Selector Depth¶
Regular and predicate selectors can also be narrowed down by specifying target's depth. Both selector types allow specifying depth as an integer value:
In addition, predicate selectors also support specifying depth as a predicate:
We will use the following class structure for the examples:
where the classes are defined as:
record Root(A a, B b) {}
record A(String value) {}
record B(A a1, A a2, C c) {}
record C(A a, D d) {}
record D(A a) {}
In the first few examples, we will target class A
at different levels:
A
at depth 1Root root = Instancio.of(Root.class)
.set(Select.all(A.class).atDepth(1), new A("Hello!"))
.create();
=> Root[a=A[value="Hello!"], b=B[a1=A[value="BRHD"], a2=A[value="AVBMJRP"], c=C[a=A[value="PZK"], d=D[a=A[value="AQVXCT"]]]]]
Similar to the above, but omitting Instancio.of()
for brevity:
A
nodes at depth 2Select.all(A.class).atDepth(2)
=> Root[a=A[value="FNPI"], b=B[a1=A[value="Hello!"], a2=A[value="Hello!"], c=C[a=A[value="IDLOM"], d=D[a=A[value="QXPW"]]]]]
A
nodes at depths 3 and 4 (and beyond, if any)Select.types().of(A.class).atDepth(depth -> depth > 2)
=> Root[a=A[value="MWAASZU"], b=B[a1=A[value="ODSRTG"], a2=A[value="TDG"], c=C[a=A[value="hello!"], d=D[a=A[value="hello!"]]]]]
A
nodes reachable from B
Select.all(A.class).within(scope(B.class))
=> Root[a=A[value="GNDUXU"], b=B[a1=A[value="hello!"], a2=A[value="hello!"], c=C[a=A[value="hello!"], d=D[a=A[value="hello!"]]]]]
The next example is targeting allStrings()
, therefore the value is being set to "Hello!"
instead of new A("Hello!")
. This snippet targets all strings that are reachable from class A
, but only if class A
is at depth 3
or greater.
Select all Strings reachable from A nodes at depth 3 or greater | |
---|---|
atDepth(N)
is converted to toScope()
, the selection matches any target at depth N
or greater.
The final example is targeting the field A.value
but only within the a1
field of class B
:
A.value
of the a1
fieldSelect.field(A::value).within(field(B::a1).toScope())
=> Root[a=A[value="DBOS"], b=B[a1=A[value="hello!"], a2=A[value="KFBWJL"], c=C[a=A[value="VLTNXF"], d=D[a=A[value="CDV"]]]]]
Selector Strictness¶
Strict Mode¶
Instancio supports two modes: strict and lenient, an idea inspired by Mockito's highly useful strict stubbing feature.
In strict mode, unused selectors will trigger an error. In lenient mode, unused selectors are simply ignored. By default, Instancio runs in strict mode. This is done for the following reasons:
- to eliminate errors in data setup
- to simplify fixing tests after refactoring
- to keep test code clean and maintainable
Eliminate errors in data setup¶
An unused selector could indicate an error in the data setup. As an example, consider populating the following POJO:
At first glance, we might expect a Set
of size 10 to be generated. However, since the field is declared as a SortedSet
and the class selector targets Set
, the generate()
method will not be applied.
Since Select.all(Set.class)
did not match any target, Instancio produces an error:
org.instancio.exception.UnusedSelectorException:
-> Unused selectors in generate(), set(), or supply():
1: all(Set)
Without being aware of this detail, it is easy to make this kind of error and face unexpected results even with a simple class like the above. It gets trickier when generating more complex classes. Strict mode helps reduce this type of error.
Simplify fixing tests after refactoring¶
Refactoring always causes tests to break to some degree. As classes and fields get reorganised and renamed, tests need to be updated to reflect the changes. Assuming there are existing tests utilising Instancio, running tests in strict mode will quickly highlight any problems in data setup caused by refactoring.
Keep test code clean and maintainable¶
Last but not least, it is important to keep tests clean and maintainable. Test code should be treated with as much care as production code. Keeping the tests clean and concise makes them easier to maintain.
Lenient Mode¶
While strict mode is highly recommended, Instancio provides a few options to disable checking for unused selectors. The following are the possible options, with the least recommended last:
- At selector level, by marking an individual selector as lenient
- At object level, by treating all selectors as lenient
- Via Settings
- Globally via
instancio.properties
The first option is shown below, where the selector is marked as lenient()
:
Person person = Instancio.of(Person.class)
.set(Select.fields().named("someFieldThatMayNotExist").lenient(), "some value")
.create();
The second option is to enable lenient mode for all selectors:
Person person = Instancio.of(Person.class)
.set(Select.fields().named("someFieldThatMayNotExist"), "some value")
.set(Select.fields().named("anotherFieldThatMayNotExist"), "another value")
.lenient()
.create();
Alternatively, lenient mode can be enabled using Settings
:
Settings
Settings settings = Settings.create()
.set(Keys.MODE, Mode.LENIENT);
Person person = Instancio.of(Person.class)
.withSettings(settings)
// snip... same selectors as above
.create();
Lastly, lenient mode can also be enabled globally using instancio.properties
. This is the least recommended option.
Customising Objects¶
Properties of an object created by Instancio can be customised using
methods defined in the InstancioApi class.
For brevity, the following examples assume a static import for selectors import static org.instancio.Select.*
Using generate()
¶
The generate()
method provides access to built-in generators for core types from the JDK, such as strings, numeric types, dates, arrays, collections, and so on. It allows modifying generation parameters for these types in order to fine-tune the data. The usage is shown in the following example, where the gen
parameter (of type Generators) exposes the available generators to simplify their discovery using IDE auto-completion.
Example of using generate() | |
---|---|
Below is another example of customising a Person
. For instance, if the Person
class has a field List<Phone>
, by default Instancio would use ArrayList
as the implementation. Using the collection generator, this can be overridden by specifying the type explicitly:
Example: customising a collection | |
---|---|
Each generator provides methods applicable to the type it generates, for example:
gen.string().minLength(3).allowEmpty().nullable()
gen.map().size(5).nullableValues().subtype(TreeMap.class)
gen.temporal().localDate().future()
gen.longs().min(Long.MIN_VALUE)
gen.enumOf(MyEnum.class).excluding(MyEnum.FOO, MyEnum.BAR)
See Built-in Generators for a list of available generators
In addition, most generators can also map values to a different type. For example, the following returns generated values as Strings:
Using set()
¶
The set()
method can be used for setting a static value to selected targets, just like a regular setter method:
However, unlike a regular set method that can only be invoked on a single object, the above will set countryCode
to "+1" on all generated instances of Phone
class. Assuming the Person
class contains a List<Phone>
, they will all have the specified country code. Similarly, all LocalDateTime
values will be set to the same instance of now()
.
Using supply()
¶
The supply()
method has two variants:
The first accepts a java.util.function.Supplier
and is for supplying non-random values. The second accepts an Instancio Generator (a functional interface) and can be used for supplying random values.
Using supply() to provide non-random values¶
The following is another example of setting all LocalDateTime
instances to now()
.
Unlike the earlier example using set()
, this will assign a new instance for each LocalDateTime
:
Using supply() to provide random values¶
The second variant of the supply()
method can be used for supplying random values and objects. This method takes a Generator as an argument, which is a functional interface with the following signature:
Using the provided Random instance ensures that generated objects are reproducible. Since Generator
is a functional interface it can be specified as a lambda expression:
Generators can be used for generating simple value types as well as building complex objects. They are described in more detail in the Custom Generators section.
Summary of methods for customising objects¶
Methods set(TargetSelector, Object)
and supply(TargetSelector, Supplier)
are for supplying objects as is. The provided objects are treated as read-only by the engine. The behaviour of these methods cannot be customised.
- matching selectors will not be applied
- the engine will not modify or populate any fields of the supplied object
- callbacks are not invoked on objects provided by these methods
Method supply(TargetSelector, Generator)
is for creating objects using custom Generator
implementations. This method offers configurable behaviour via the AfterGenerate
hint. By default, the hint is set to POPULATE_NULLS_AND_DEFAULT_PRIMITIVES
, which implies:
- matching selectors will be applied
- the engine will populate
null
fields and primitive fields containing default values
The default value of the AfterGenerate
hint can be overridden using instancio.properties
and Settings.
Info
Callbacks are always invoked on objects created by generators regardless of AfterGenerate
value.
Method generate(TargetSelector, GeneratorSpecProvider)
is for customising objects created by internal generators. Such objects include value types (numbers, strings, dates) and data structures (collections, arrays).
- matching selectors are always applied
- matching callbacks are always invoked
By default, the engine generates non-null values, unless specified otherwise.
Using assign()
¶
Experimental API @since 3.0.0
The assignment API allows customising an object by passing in one or more Assignment
objects as a vararg:
Assignments can be created using static methods provided by the Assign class. It provides three entry-point methods for creating an assignment:
Each Assignment
may have:
- an origin selector - the source of the assignment; must match exactly one value
- destination selector(s) whose target(s) will be assigned a given value
Generator
for generating the value to assignPredicate
that must be satisfied by the origin for the assignment to be appliedFunction
for mapping the value before assigning to destination targets
The three types of expressions can be used for different use-cases described in the following sections.
Assign.valueOf(target)
¶
This builder provides two options:
- Assign value directly to the
target
- Assign value of the
target
to another selector
The first option is to assign a value directly to the target
selector using set()
, supply()
, or generate()
methods as shown in the following example:
Assignment[] assignments = {
Assign.valueOf(Person::getName).set("Bob"),
Assign.valueOf(Person::getAge).generate(gen->gen.ints().range(1,100))
};
Person person = Instancio.of(Person.class)
.assign(assignments)
.create();
The second option is to set the value of the target
to another selector. For example, the following snippet assigns the value of Person.firstName
to Person.preferredName
:
This method supports an optional predicate to specify when the assignment should be applied. In the following example, the preferred name is set to the first name only if the first name starts with "A". Otherwise, the preferred name will be set to a random value.
In addition, the expression allows specifying a Function
for transforming the result. For example, the following snippet sets the preferred name to the same value as the first name, unless the first name is Robert, in which case the preferred name will be set to Bob:
Assign.given(origin)
¶
This expression allows setting different destination selectors with different values based on a given origin and predicate. The following snippet sets cancellation-related order data when the generated order status is CANCELLED
:
Assign.given(origin, destination)
¶
This expression allows mapping predicates to different values for a given pair of origin and destination selectors:
The When class provides convenience methods for creating predicates. In addition to set(predicate, value)
method shown above, this API also supports supply()
and generate()
, as well as the following optional methods to specify an else
action if none of the predicates match:
elseSet()
elseSupply()
elseGenerate()
Assignment Restrictions¶
Origin selector restrictions¶
When using assignments, the origin selector must match a single target. For this reason, the origin does not support selector groups and primitive/wrapper selectors, such as allInts()
.
For example, Assign.given(allStrings())
is acceptable for the following class because there is only one string that matches the selector:
but not for the following classes because allStrings()
would match more than one string value:
record MyRecord(String string1, String string2, Integer number) {}
record MyRecord(List<String> string, Integer number) {}
Convenience primitive/wrapper selectors, such as allInts()
, are not accepted by assignments as they are a shorthand for Select.all(all(int.class). all(Integer.class))
. Assignment expressions must specify either the primitive, or the wrapper type explicitly: Assign.given(all(Integer.class))
.
Collection restrictions¶
Assignments have an additional restriction when used with collections. If the origin selector of an assignment targets a collection element, then destination selector(s) must be within the collection. Assuming we have the following data model:
class Person {
String name;
String countryOfCitizenship;
List<Address> addresses;
}
class Address {
String street;
String city;
String country;
}
It is possible to create an assignment based on address city
and fields country
:
Since Address
is a collection element, the assignment is scoped to each Address
instance. The following usage, on the other hand, is invalid:
Invalid usage | |
---|---|
This is because Address
is a collection element, therefore, valueOf(Address::getCountry)
matches multiple values. For this reason, no guarantee is made as to the value that will be assigned to Person.countryOfCitizenship
field.
Using onComplete()
¶
Another option for customising generated data is using the OnCompleteCallback, a functional interface with the following signature:
The OnCompleteCallback
is invoked after the generated object has been fully populated. Callbacks can be triggered only for objects:
- created internally by the engine
- created by custom generators
Callbacks are never invoked on objects provided using:
supply(TargetSelector, Supplier)
set(TargetSelector, Object)
The following example demonstrates how an Address
object can be modified using a callback. If a Person
has a List<Address>
, the callback will be invoked for every instance of the Address
class that was generated.
Example: modifying an object via a callback | |
---|---|
If the object is mutable, callbacks allow modifying multiple fields at once. However, callbacks cannot be used to modify immutable types.
Another property of callbacks is that they are only invoked on non-null objects. In the following example, all address instances are nullable. Therefore, a generated address instance may either be null
or a fully-populated object. However, if a null
was generated, the callback will not be invoked.
Callbacks are only called on non-null values | |
---|---|
Using filter()
¶
Experimental API @since 4.6.0
This method can be used to filter generated values using a predicate. If the predicate evaluates to false
for a given value, Instancio will generate a new value. The new value will also be tested against the predicate, and so on.
A simple example is to generate a list of even numbers:
Example: generate even numbers | |
---|---|
A more realistic use case is to ensure that certain fields of a generated object have unique values. For instance, we may want to generate a list of Person
objects with unique string and numeric values. If the Person
contains numeric IDs, this will ensure that the generated ID values are distinct across all instances:
Example: generate an object with unique values | |
---|---|
This is how withUnique()
API is implemented.
It should be noted that using the filter()
method can be inefficient if the probability of generating a random value that would be rejected by the predicate is high. This is because a value that does not satisfy the predicate must be generated again. This results in many values being generated and discarded.
Maximum retries
An exception will be thrown if the number of retries to generate a value for a given node exceeds 1000
.
In addition. it is not recommended to use filter()
with POJOs or collections. For example, if isActive()
returns false
in the following snippet, the entire User
object will be generated from scratch:
Not recommended! | |
---|---|
Therefore, customising objects using other APIs, such as generate()
should be preferred over filter()
, if possible.
Using withUnique()
¶
Experimental API @since 4.8.0
This method can be used to generate unique values for a given selector's targets. Assuming the following class:
The snippet below will generate a list of Data
instances with unique foo
values:
Note that the values will be unique across all targets that match the selector. For instance, the following usages:
withUnique(allInts())
withUnique(all(field(Data::foo), field(Data::bar))
would result in unique values for foo
and bar
with no overlap (i.e. foo
and bar
are disjoint). To generate unique values per field (with potential overlap), the withUnique()
method must be specified per field:
42
was generated for both fields.
If it is impossible to generate a sufficient number of unique values after a certain number of attempts, an exception will be thrown:
Using setBlank()
¶
Experimental API @since 4.7.0
This method can be used to initialise certain parts of an object to be blank using selectors. Its behaviour is the same as the methods below (which can be used for creating a blank root object):
Instancio.createBlank(Class<T>)
Instancio.ofBlank(Class<T>)
Blank objects have the following properties:
- value fields (such as strings, numbers, dates) are
null
- arrays, collections, and maps are empty
- references to POJOs are initialised to blank objects
For example, assuming the Person
class below (getters and setters omitted):
The following snippet
of(Person.class)
and not ofBlank(Person.class)
, as the latter would create a blank root object.
will produce a partially blank object, where only the name
and dateOfBirth
fields are populated with random values. The address
field has been set to a blank object and phoneNumbers
to an empty List
:
// Person[
// name=NOBXGV,
// dateOfBirth=2022-03-18,
// phoneNumbers=[]
// address=Address[street=null, city=null, country=null]
// ]
Blank objects can be customised further if needed. For example, we can set the country field to "Canada", generate a collection of blank phones of size 2
, and set the countryCode
to +1
as shown below:
Using ignore()
¶
By default, Instancio will attempt to populate every non-static field value. The ignore
method can be used where this is not desirable:
Example: ignoring certain fields and classes | |
---|---|
Precedence of ignore()
¶
The ignore()
method has higher precedence than other methods. For example, in the following snippet specifying ignore(all(LocalDateTime.class))
but supplying a value for the lastModified
field will actually generate a lastModified
with a null
value.
Example | |
---|---|
Usage of ignore()
with Java records¶
When ignore()
is used to target one of the required arguments of a record
constructor, then a default value for the ignored type will be generated.
Example | |
---|---|
Nullable Values¶
By default, Instancio generates non-null values. There are cases where this behaviour may need to be relaxed, for example, to verify that a piece of code does not fail in the presence of certain null
values. There are a few ways to specify that values can be nullable which can broadly be grouped into two categories:
- field nullability
- array/collection element and map key/value nullability
Field nullability¶
Field declarations can be made nullable using:
- withNullable(TargetSelector) method of the builder API
- generator spec
nullable()
method (for generators that support it) - Settings
For example, the following marks Person.address
and all String
fields as nullable:
Some built-in generators also support marking values as nullable using the nullable()
method. As in the previous example, this also applies only to string fields, and not, for example, List<String>
:
The final example, using Settings
, follows the same principle:
Element nullability¶
Data structure elements can be made nullable using:
- array generator:
nullableElements()
- collection generator:
nullableElements()
- map generator:
nullableKeys()
,nullableValues()
- Settings keys
Keys.ARRAY_ELEMENTS_NULLABLE
Keys.COLLECTION_ELEMENTS_NULLABLE
Keys.MAP_KEYS_NULLABLE
Keys.MAP_VALUES_NULLABLE
Example: specifying nullability using the collection generator | |
---|---|
Assuming the Person
class contains a Map
, nullability can be specified for keys and values:
Example: specifying nullability using the map generator | |
---|---|
Lastly, element nullability can be specified using Settings as follows:
Example: specifying nullability using Settings | |
---|---|
Subtype Mapping¶
Subtype mapping allows mapping a type to its subtype. This can be used for:
- specifying an implementation class for an abstract class or interface
- testing behaviour using different implementations
By default, Instancio does not resolve implementation classes of an abstract type. The only exceptions to this are:
- JDK classes, such
List
,Map
,CharSequence
, etc. (which default toArrayList
,HashMap
, andString
, respectively). sealed
interfaces and abstract classes (which default to a random implementation).
For user-defined abstract types, the implementation class must be specified via the API. If not specified:
- a
null
value will be generated where the abstract type is a field - an empty collection will be generated if the abstract type is a collection element
- an exception will be thrown if the root type is abstract and no subtype is specified
Specifying Subtypes¶
The mapping can be specified using the subtype()
method provided by InstancioApi:
or the mapType()
method declared by the Settings class:
Functionally, the two methods are equivalent. However, one may be preferable over another depending on the use case.
The subtype()
method
- has higher precedence than the one offered by
Settings
- provides more flexibility as it can be applied to any selector target, such as fields
The Settings.mapType()
method, on the other hand
- can only be mapped based on class
- is reusable since an instance of
Settings
can be shared across multiple test methods - allows subtypes to be specified via
instancio.properties
and applied automatically
The following is an example of using the subtype()
method:
Address
is the superclass of AddressImpl
.
The same can be specified using Settings
:
subtype(field(Person::getPet), Cat.class)
, since the field selector is a more specific target.
Finally, when specifying the mapping via instancio.properties
:
subtypes are resolved automatically:
In addition to specifying subtypes using the API or properties file, subtypes can be resolved automatically by implementing the InstancioServiceProvider
interface.
Using Models¶
A Model is a template for creating objects. It encapsulates all the parameters specified using the builder API. Once a model is defined, it can be used to create objects without duplicating the common properties.
Model
class itself does not expose any public methods, and its instances are effectively immutable.
Objects created from a model inherit all the model's properties, including Settings, Mode and seed value.
A model can also be used as a template for creating other models. Using the previous example, we can define a new model with additional data:
Having a common model allows test methods to create custom objects by overriding the model's properties via selectors (including properties of nested objects). This also works for immutable objects, such as Java records.
This approach reduces duplication and simplifies data setup, especially for complex classes with many fields and relationships. More details on the benefits of using models, including a sample project, are provided in the article Creating object templates using Models.
Using setModel()
¶
Experimental API @since 4.4.0
The method setModel(TargetSelector, Model)
allows applying a model to another object using a selector.
To illustrate with an example, we will assume the following classes:
Given a model of Foo
:
The model can be applied to a specific Foo
field declared by the Container
:
setModel()
works by applying selectors defined within the model to the target object. In doing so, it narrows down the scope of selectors defined in the model, such as field(Foo::value)
, to the model's target field(Container::fooA)
, as shown in this diagram:
In other words, the Container
creation example is equivalent to:
setModel()
works for all Instancio API methods that accept TargetSelector
as an argument, such as assign()
, generate()
, ignore()
, and so on. However, the following properties of the Model
are not applied to the target object:
Settings
lenient()
mode- custom seed value, if any
Overriding Selectors Defined by the Model
¶
When creating an object, it is possible to override selectors defined within the model. Building on the previous example, we can override field(Foo::value)
as follows:
Model
.
In this example, the overriding selector is the same as the model's selector (including the scope). If instead of:
we specify:
Then the output will be:
In addition, the original selector defined within the model will trigger an unused selector error. In such cases, the model's selector can be marked as lenient()
:
The reason for the error is that there are now two different selectors that match the same node Foo.value
. This can be verified by calling verbose()
, which will output all the selectors and matching nodes:
Will output:
Selectors and matching nodes, if any:
-> Method: generate(), set(), or supply()
- field(Foo, "value")
\_ Node[Foo.value, depth=2, type=String]
- field(Foo, "value").within(scope(Container, "fooA")).lenient()
\_ Node[Foo.value, depth=2, type=String]
The output confirms that there are two different selectors matching the node Foo.value
, therefore selector with the lowest precedence will the trigger unused selector error unless it is marked as lenient()
.
In summary, when a Model
is provided to the setModel()
method, the selectors defined within the model will be subject to the usual selector precedence rules.
Custom Generators¶
Every type of object Instancio generates is through an implementation of the Generator
interface. A number of internal generators are included out of the box for creating strings, numeric types, dates, collections, and so on. Custom generators can also be defined to satisfy certain use cases:
Generating types not supported out of the box, for example from third-party libraries such as Guava.
Creating pre-initialised domain objects. Some domain objects require to be constructed in a certain state to be valid. To avoid duplicating the construction logic across different tests, it can be encapsulated by a custom generator that can be reused across the project.
Distributing generators as a library that can be shared across projects.
The Generator
is a functional interface with a single abstract method generate(Random)
:
If a generator produces random data, it must use the provided Random instance to guarantee that the created object can be reproduced for a given seed value. The hints()
method is for passing additional instructions to the engine. The most important hint is the AfterGenerate action which determines whether the engine should:
- populate uninitialised fields
- modify the object by applying matching selectors (if any)
An example of specifying hints | |
---|---|
In short, the AfterGenerate
enum defines what should happen to an object returned by a generator. The exact semantics of AfterGenerate
actions vary depending on the type of object. Different rules are applied to POJOs, arrays, collections, and records. For example, if a generator returns an instance of a record
, the returned instance cannot be modified regardless of the AfterGenerate
hint.
The following describes the actions as they apply to POJOs:
DO_NOT_MODIFY
Indicates that the object created by the generator should not be modified. The engine will treat the object as read-only and assign it to the target field as is. Matching selectors will not be applied.
APPLY_SELECTORS
Indicates that the object can only be modified via matching selectors using
set()
,supply()
, andgenerate()
methods.POPULATE_NULLS
Indicates that
null
fields declared by the object should be populated. In addition, the object will be modifiable using selectors as described by above byAPPLY_SELECTORS
.POPULATE_NULLS_AND_DEFAULT_PRIMITIVES
(default behaviour)Indicates that primitive fields with default values declared by the object should be populated. In addition, the behaviour described by
POPULATE_NULLS
applies as well. Default primitives are defined as:0
for all numeric typesfalse
forboolean
'\u0000'
forchar
POPULATE_ALL
Indicates that all fields should be populated, regardless of their initial values. This action will cause all the values to be overwritten with random data. This is the default mode the engine operates in when using internal generators.
When defining custom array or collection generators, the following hints can also be used:
Custom Generator Example¶
To illustrate the above with an example, consider the following generator that creates an instance of a Phone
:
Since no hints()
are specified, the default AfterGenerate
action is POPULATE_NULLS_AND_DEFAULT_PRIMITIVES
. Therefore, the generated object can be customised using selectors:
This will produce an object like Phone[countryCode="+55", number="83703291"]
, where the number
is set by the PhoneGenerator
, and the countryCode
is overridden by applying the selector. For certain use cases, it may be necessary to prevent deliberate or accidental modification of generated objects. In such cases, the generator can include the DO_NOT_MODIFY
hint as shown below:
In summary, a generator can instantiate an object and instruct the engine what should be done with the object after generate()
method returns using the AfterGenerate
hint.
Generating record
objects
It should be noted that if a generator returns an instance of a record
, then the created object cannot be modified regardless of the AfterGenerate
hint. This is due to immutability of records, since they cannot be modified after construction.
Custom generators can also be specified using the Instancio Service Provider Interface
Assignment Settings¶
Assignment settings control whether values are assigned directly to fields (default behaviour) or via setter methods. There are a few setting Keys that control the behaviour.
Keys constant | Value type | Default | Description |
---|---|---|---|
ASSIGNMENT_TYPE | AssignmentType | FIELD | Should values be assigned via fields or setters |
SETTER_STYLE | SetterStyle | SET | Naming convention used for setters |
ON_SET_FIELD_ERROR | OnSetFieldError | IGNORE | What should happen if field assignment fails |
ON_SET_METHOD_ERROR | OnSetMethodError | ASSIGN_FIELD | What should happen if method assignment fails |
ON_SET_METHOD_NOT_FOUND | OnSetMethodNotFound | ASSIGN_FIELD | What should happen if a field does not have a matching setter |
ON_SET_METHOD_UNMATCHED | OnSetMethodUnmatched | IGNORE | What should happen if a setter does not have a matching field |
SETTER_EXCLUDE_MODIFIER | int | 0 (none) | Which setters should be ignored based on method modifiers |
OVERWRITE_EXISTING_VALUES | boolean | true | Should initialised fields be overwritten with random values |
To enable assignment via methods, Keys.ASSIGNMENT_TYPE
can be set to AssignmentType.METHOD
. This setting only applies to mutable fields because final
fields cannot have setters. For non-static final
fields Instancio will fall back to AssignmentType.FIELD
.
When method assignment is enabled, Instancio will attempt to resolve setter names from field names using SETTER_STYLE
setting. This key's value is the SetterStyle
enum that supports three naming conventions:
SET
- standard setter prefix:setFoo("value")
WITH
- for example:withFoo("value")
PROPERTY
- no prefix:foo("value")
There might be cases where a setter does not match any field using the configured naming convention. ON_SET_METHOD_UNMATCHED
determines what happens in such cases. The default value is OnSetMethodUnmatched.IGNORE
, therefore unmatched setters will not be invoked. The behaviour can be overridden by setting the value to OnSetMethodUnmatched.INVOKE
.
Special care must be taken when enabling unmatched setters in the presence of overloaded setters.
SETTER_EXCLUDE_MODIFIER
specifies whether setters with certain method modifiers should be ignored (by default, there are no exclusions). For example, using this setting it is possible to instruct Instancio to ignore private and package-private setters.
The remaining ON_SET_*
keys are used to control error-handling behaviour:
Key | Possible causes |
---|---|
ON_SET_FIELD_ERROR | type mismatch, unmodifiable field, access exception |
ON_SET_METHOD_ERROR | type mismatch, an exception is thrown by setter (e.g. due to validation) |
ON_SET_METHOD_NOT_FOUND | method does not exist, or the name does not conform to the naming convention defined by SetterStyle |
All of the above can be set to ignore errors or fail fast by raising an exception. In addition, both ON_SET_METHOD_*
settings can be configured to fall back to field assignment in case of an error.
Setting Keys.ON_SET_FIELD_ERROR
to OnSetFieldError.IGNORE
An error caused by assigning an incompatible type is considered a user error and is never ignored.
For example, attempting to set(allStrings(), 12345)
will always trigger an error regardless of the ON_SET_FIELD_ERROR
setting.
The following snippet illustrates how to create an object populated via setters. In this example, SetterStyle.PROPERTY
is used since the Phone
class has setters without the set prefix:
Settings can be specified per object, as shown above, or globally using a properties file:
See Configuration for details.
Unmatched Setters¶
When Keys.ASSIGNMENT_TYPE
is set to METHOD
, Instancio parses fields and setters declared by a class. In addition, it attempts to match each field to the corresponding setter using its name and parameter type. If a setter does not have a matching field, it is treated as unmatched. This can be illustrated using the following Person
class.
The class has the setName(String)
method that matches the String name
field. In addition, it has the setFavouriteFood(String)
method. This setter is unmatched because it does not have a corresponding String favouriteFood
field.
Field | Setter | |
---|---|---|
String name | setName(String) | Matched setter |
- | setFavouriteFood(String) | Unmatched setter |
Using OnSetMethodUnmatched.INVOKE
¶
In the first example, we will create an object with the following settings and verbose()
mode enabled:
verbose()
mode prints the node hierarchy to standard out:
<0:Person>
├──<1:Person: Map<String, String> attributes> // Field without a setter
│ ├──<2:String> // map key
│ └──<2:String> // map value
├──<1:Person: String name; setName(String)> // Field with a setter
└──<1:Person: setFavouriteFood(String)> // Setter without a field (unmatched setter)
The following is a sample object generated by the snippet:
The attributes
map was not populated with random entries because ON_SET_METHOD_NOT_FOUND
was set to OnSetMethodNotFound.IGNORE
. However, since ON_SET_METHOD_UNMATCHED
was set to OnSetMethodUnmatched.INVOKE
, Instancio invoked the setFavouriteFood
method passing in a random value. Therefore, the map contains a FAVOURITE_FOOD
entry.
Next, we will set ON_SET_METHOD_NOT_FOUND
to OnSetMethodNotFound.ASSIGN_FIELD
, keeping everything else the same:
This time, a sample output might look as follows:
In the above example, Instancio generates a map with random values and assigns it directly via the field since it does not have a setter. setFavouriteFood
is also invoked, which adds the FAVOURITE_FOOD
entry to the map.
Using OnSetMethodUnmatched.IGNORE
¶
In the next example, we modify the above snippet to set ON_SET_METHOD_UNMATCHED
to OnSetMethodUnmatched.IGNORE
:
Since unmatched setters are ignored, the node hierarchy does not have the <1:Person: setFavouriteFood(String)>
node:
<0:Person>
├──<1:Person: Map<String, String> attributes>
│ ├──<2:String>
│ └──<2:String>
└──<1:Person: String name; setName(String)>
The sample output contains an empty map since the unmatched setter was not invoked:
If we modify the last example to set Keys.ON_SET_METHOD_NOT_FOUND
to OnSetMethodNotFound.ASSIGN_FIELD
, then the attributes
map will be populated with random entries but no FAVOURITE_FOOD
.
Overloaded unmatched setters¶
Special care must be taken when a field has more than one setter. Consider the following class with two setValue
methods:
At first glance, the following example should produce the value
field set to 123
:
However, running the snippet will produce the following output:
This is because ON_SET_METHOD_UNMATCHED
is set to OnSetMethodUnmatched.INVOKE
, which results in two nodes related to the value
field:
<0:Pojo>
├──<1:Pojo: int value; setValue(int)> // field with matching setter
└──<1:Pojo: setValue(double)> // unmatched setter without a field
Instancio would populate each of these nodes. Nodes that have fields are populated first, and nodes without fields (unmatched setters) are last. The reason for this is that unmatched setters often require fields to be initialised before being invoked (for instance, an unmatched setter might be adding an element to a collection).
Therefore, for the above snippet to produce the expected value of 123
, the setValue(double)
method would need to be ignored (or ON_SET_METHOD_UNMATCHED
set to OnSetMethodUnmatched.IGNORE
):
setter(Pojo.class, "setValue", double.class)
must be used to specify the parameter explicitly, instead of the more concise setter(Pojo::setValue)
.
Initialised Fields¶
The setting Keys.OVERWRITE_EXISTING_VALUES
controls whether Instancio can overwrite initialised fields with random values. "Initialised" is defined as having a non-default value. Default values are:
0
forbyte
,short
,int
,long
,float
,double
, andchar
false
forboolean
null
forObject
Below are a few examples of how this setting works using the following class (getters and setters omitted):
By default, OVERWRITE_EXISTING_VALUES
is set to true
. As a result, Instancio overwrites initialised fields with random values.
When OVERWRITE_EXISTING_VALUES
is set to false false
, the initialised value is preserved.
Preserve initialised values | |
---|---|
Finally, regardless of the OVERWRITE_EXISTING_VALUES
setting, initialised values can be overwritten using a selector.
Overwrite initialised value using a selector | |
---|---|
Maximum Depth Setting¶
This setting controls the maximum depth for populating an object. Instancio will populate values up to the maximum depth. Beyond that, values will be null
unless the maximum depth is set to a higher value.
The count starts from the root object, which is at depth 0. Children of the root object are at depth 1, grandchildren at depth 2, and so on. The default value is defined by the Keys.MAX_DEPTH
setting key.
The primary reasons for modifying this setting are:
To improve the performance.
The performance may be inadequate when generating data for large complex classes. Consider reducing the maximum depth value in such cases (and also, using
ignore()
to exclude certain objects).To generate data beyond the default maximum depth.
If the default maximum depth is not sufficient to fully populate an object, consider increasing the value.
Modifying Maximum Depth¶
The setting can be set to a custom value using one of the following options, from lowest to highest precedence.
Using instancio.properties
to define a new global maximum depth:
Using Settings with Keys.MAX_DEPTH
key.
Settings settings = Settings.create().set(Keys.MAX_DEPTH, 15);
Person person = Instancio.of(Person.class)
.withSettings(settings)
.create();
Using the API method withMaxDepth(int):
Cyclic Objects¶
Data models often have circular relationships. A common example is a one-to-many relationship among JPA entities. Consider the following example where each OrderItem
references the Order
to which it belongs (getters and setters omitted for brevity):
The default behaviour of Instancio is to terminate cycles with a null
reference. For example, the following snippet will produce OrderItem.order
references set to null
:
It is, however, possible to set a back-reference to the root object instead of generating null
. One way to accomplish this is using the assign()
API:
An alternative option is to assign back-references automatically using the Keys.SET_BACK_REFERENCES
setting. The following snippet will produce the same result as the example above:
Settings
or instancio.properties
When this setting is enabled, Instancio will set the OrderItem.order
reference to a previously generated Order
instance. In this example it happens to be the root object.
It should be noted that in certain cases, enabling SET_BACK_REFERENCES
may produce unwanted results. Consider the following example of creating an OrderItem
:
This will produce the following objects:
graph LR
A[OrderItem] --> B[Order];
B --> C["List&lt;OrderItem&gt;"];
where all elements of List<OrderItem>
are the same instance of the OrderItem
.
Cartesian Product¶
Experimental API @since 4.0.0
The following methods are the entry points for generating the Cartesian product:
Cartesian Product API | |
---|---|
Inputs can be specified using the following method:
As an example, consider the snippet below.
This will produce a list containing 9 results in lexicographical order:
[Widget[type=FOO, num=1],
Widget[type=FOO, num=2],
Widget[type=FOO, num=3],
Widget[type=BAR, num=1],
Widget[type=BAR, num=2],
Widget[type=BAR, num=3],
Widget[type=BAZ, num=1],
Widget[type=BAZ, num=2],
Widget[type=BAZ, num=3]]
Limitations¶
The selector passed to the with()
method must match a single target. For example, the target cannot be a collection element:
The above will produce an error with a message: "no item is available to emit()"
.
Bean Validation¶
Instancio can generate valid data based on Bean Validation annotations. This is an experimental feature and is disabled by default.
The feature can be enabled via Settings using Keys.BEAN_VALIDATION_ENABLED
, or globally, using instancio.properties
:
Instancio supports annotations from:
jakarta.validation.constraints
javax.validation.constraints
org.hibernate.validator.constraints
It will generate data based on the constraints, depending on what is available on the classpath. Instancio does not provide the dependencies transitively.
By default, Instancio reads annotations from fields. The behaviour can be customised to read annotations from getters instead. This can be done using the Keys.BEAN_VALIDATION_TARGET
setting or instancio.properties
:
Supported Annotations¶
The list of supported annotations is provided below. Anything not listed is unsupported, including *.List
annotations.
Jakarta or Javax¶
@AssertFalse
@AssertTrue
@DecimalMax
@DecimalMin
@Digits
@Email
@Future
(not supported byMonthDay
)@FutureOrPresent
(delegates toFuture
)@Max
@Min
@Negative
@NegativeOrZero
@NotBlank
(delegates toNotEmpty
)@NotEmpty
@NotNull
@Past
(not supported byMonthDay
)@PastOrPresent
(delegates toPast
)@Positive
@PositiveOrZero
@Size
Hibernate¶
Standard¶
@CreditCardNumber
@DurationMin
@DurationMax
@EAN
@ISBN
@Length
@LuhnCheck
(ignoreNonDigitCharacters
is ignored, only digits are generated)@Mod10Check
(ignoreNonDigitCharacters
is ignored, only digits are generated)@Mod11Check
(ignoreNonDigitCharacters
is ignored, only digits are generated)@Range
@UniqueElements
@URL
(protocol
,host
,port
are supported;regexp
not supported)@UUID
Brazilian¶
@CNPJ
@CPF
@TituloEleitoral
Polish¶
@NIP
@PESEL
@REGON
Russian¶
@INN
JPA¶
In addition to Bean Validation, Instancio supports generating data based on JPA @Column
annotation. This is an experimental feature available from version 3.3.0
. It is disabled by default and can be enabled via Settings using Keys.JPA_ENABLED
, or globally, using instancio.properties
:
In addition, jakarta
or javax
API must be present on the classpath for the feature to be activated. Instancio does not provide the dependency transitively:
<dependency>
<groupId>jakarta.persistence</groupId>
<artifactId>jakarta.persistence-api</artifactId>
<version>${jakarta-persistence-api-version}</version>
</dependency>
or
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>javax.persistence-api</artifactId>
<version>${javax-persistence-api-version}</version>
</dependency>
Supported Attributes¶
The following @Column
attributes are supported:
precision
- supported byFloat
,Double
, andBigDecimal
fields (with limitations described below)scale
- supported byFloat
,Double
, andBigDecimal
fieldslength
- supported byString
fields
Limitations¶
If used with Keys.BEAN_VALIDATION_ENABLED
, Bean Validation annotations take precedence. For instance, the precision
attribute is not honoured if a field is annotated with any of the following Bean Validation/Hibernate Validator annotations:
@Min
,@Max
@DecimalMin
,@DecimalMax
@Negative
,@NegativeOrZero
@Positive
,@PositiveOrZero
@Range
To illustrate with an example, consider the following field declaration:
precision = 5, scale = 3
implies a range of [10.000, 99.999]
, whereas the @Min
and @Max
limit the range to [1, 7]
. Since the @Min
and @Max
annotations take precedence, the precision
attribute will be ignored, and the generated value
will be between 1.000
and 7.000
, inclusive, and have the specified scale of 3
.
Data Feeds¶
Experimental API @since 5.0.0
Some tests are better expressed when test data is defined in an external resource. This is especially true when tests require a large or complex dataset, making it impractical to define the data within the test class. The Feed interface has been added to simplify working with data from external sources.
There are several methods for creating a feed, detailed in the following sections. For demonstration purposes, we will use a persons.csv
file located at the root of the classpath (e.g., src/test/resources/persons.csv
):
The sample data is referenced in all subsequent examples.
firstName, lastName, dateOfBirth, gender
John, Doe, 2001-11-22, MALE
Alice, Smith, 1977-07-15, FEMALE
Bob, Brown, 1999-12-01, MALE
# snip...
and the Person
class defined as:
Using applyFeed()
¶
The applyFeed()
method allows creating a feed using the FeedProvider
API and mapping the feed data to a POJO or a record
using a selector:
Here's an example that maps data from the persons.csv
file to Person
objects:
The mapping works as follows:
- Data properties are mapped to matching field names declared by the class.
- Class fields that do not have a corresponding property in the data file are populated with random values (such as the
id
field in the previous example). - If a data property does not map to any field in the target class, an exception will be thrown. To ignore unmatched properties, set the
Keys.ON_FEED_PROPERTY_UNMATCHED
setting toOnFeedPropertyUnmatched.IGNORE
.
Custom Feeds¶
Custom feeds can be created by extending the Feed interface:
Defining a custom feed allows reusing it across multiple tests, avoiding the need to hard-code the file name:
In addition to passing thePersonFeed
class to the applyFeed()
method, we can also instantiate an instance of the feed using the methods below. The first method is a shorthand API, while the second is a builder that can be used to customise an instance of a feed:
An example of creating a feed instance | |
---|---|
While PersonFeed
does not declare any methods, it inherits several convenience methods from Feed
for accessing specific data properties, such as the lastName
. For example, to create an instance of the PersonFeed
and populate only the last names:
The stringSpec()
method is an example of a "feed spec", which will be described in more detail in the next section.
Feed Specs¶
The Feed
interface provides various methods for retrieving data. These methods return a FeedSpec<T>
and automatically convert values to the specified type T
(see Default String Conversion for the list of supported types):
FeedSpec<String> stringSpec(String propertyName);
FeedSpec<Boolean> booleanSpec(String propertyName);
FeedSpec<Integer> intSpec(String propertyName);
// ...
From here on, we will refer to methods that return a FeedSpec
as feed specs.
In addition to the built-in methods, Instancio supports defining custom feed specs. By convention, method names automatically map to the corresponding properties in the data feed.
By declaring an explicit method such as lastName()
, we can avoid calling stringSpec("lastName")
with a hard-coded property name.
To summarise, feed data can be accessed using:
- built-in feed spec methods:
stringSpec("lastName")
,intSpec("age")
- user-defined feed spec methods:
lastName()
,age()
Using Feed Specs¶
The FeedSpec
type supports a couple of different use cases. Under the hood, the FeedSpec
type is an implementation of a Generator
. Therefore, it can be passed to the supply()
and generate()
methods as demonstrated earlier. Moreover, the FeedSpec
interface extends ValueSpec, which provides methods such as get()
and list()
for generating values:
See Creating Simple Values for a summary of ValueSpec
methods.
In addition, feed spec methods support the following annotations:
@DataSpec
¶
This annotation allows mapping a data property using the annotation attribute instead of the spec method name. This decouples method names from specific data properties:
surname()
method corresponds to the lastName
property in the data feed.
This approach removes the requirement for method names to match exact data property names.
@TemplateSpec
¶
The @TemplateSpec
annotation enables the definition of spec methods using string templates with ${placeholders}
:
In the example above, placeholders like ${firstName}
and ${surname}
can directly map to data properties or other feed spec method names.
When invoking the greeting()
method with the persons.csv
data defined earlier, it produces the following output:
@GeneratedSpec
¶
The @GeneratedSpec
annotation allows defining feed specs that generate data not present in the data file, but rather by using a specified generator.
Here's an example using @GeneratedSpec
to generate random lastModified
values that are in the past:
In this example, the lastModified()
method in PersonFeed
uses @GeneratedSpec
to specify PastInstantGenerator
as the generator for Instant
values. This approach allows for generation of data that complements existing data in the feed.
@FunctionSpec
¶
The @FunctionSpec
annotation enables combining inputs from one or more data properties or spec methods to produce a result using a specified FunctionProvider
. It has two attributes:
params
- specifies the names of data properties or spec methods used as inputs.provider
- refers to a class implementingFunctionProvider
, which processes inputs and returns a result.
For example, the following snippet generates a person's bio from the firstName
, lastName
, and dateOfBirth
properties:
The @FunctionSpec.provider
annotation attribute is a reference to a class implementing the FunctionProvider
. The FunctionProvider
implementations must have exactly one method matching the @FunctionSpec.params
.
Additionally, org.instancio.Random
can be declared as the last parameter if randomisation is required. For instance, the method declared by BioProvider
can also be defined as:
where the random
instance can be used to randomise the output (if necessary) with the usual reproducibility guarantees.
If Random
is declared, it must be the last parameter.
@WithStringMapper
¶
This annotation allows you to specify a custom Function
for converting string values to a type that is not supported out of the box.
For example, suppose we need a specific property as a byte array:
byte[]
is the type returned by the mapping function.
@WithStringMapper
can also be used with methods annotated with @DataSpec
:
@WithStringMapper
is ignored when applied to @FunctionSpec
, @GeneratedSpec
, or @TemplateSpec
spec methods.
@WithPostProcessor
¶
This annotation enables post-processing of values returned by feed specs using an implementation of the PostProcessor
interface:
Using the PersonFeed
as an example, a post-processor can be used to convert names to uppercase:
Invoking the firstName()
method using the persons.csv
defined earlier produces the following output:
The @WithPostProcessor
annotation can be combined with any other annotation described earlier. In addition, a spec method can define multiple post-processors. This is shown in the following example, where two post-processors are applied to the @TemplateSpec
:
The above snippet will result in the following output:
Post-processors can also be used for fuzzing the data using the Random
instance provided as the second argument.
@NullableSpec
¶
The @NullableSpec
annotation is used to indicate that specific feed specs can return null values. This annotation can be combined with any of the other annotations mentioned earlier. When a feed spec is marked as nullable, its corresponding method may randomly produce null values. For example:
Invoking the gender()
method may yield results like:
This allows for variability in data generation scenarios where null
values are valid or desired outputs.
Feed Configuration¶
This section outlines configuration options available for feeds. Configuration can be specified through (in order of precedence, from highest to lowest):
- Builder API during feed creation.
- Annotations within a custom feed interface.
- Settings via the
Settings
class orinstancio.properties
.
Feed Data Source¶
All feeds require a data source to be specified. For custom feeds that extend the Feed
interface, this can be achieved using the @Feed.Source
annotation. Alternatively, the data source can be specified (or overridden) using the builder API. This table summarises the options:
@Feed.Source attribute | Builder API | Description |
---|---|---|
string | ofString() | Provides data as a String |
file | ofFile() | Specifies the path to a file |
resource | ofResource() | Refers to a classpath resource |
- | ofInputStream() | Provides data as an InputStream |
- | of(Class) | Uses a custom Feed class with @Feed.Source |
It's important to note that the builder API can override the data source specified by @Feed.Source
. For example, consider the PersonFeed
interface defined with a default resource:
To reuse PersonFeed
with an alternative data source:
Person
data will be populated using another-persons.csv
.
The applyFeed()
method also supports specifying the data source directly, without creating a custom Feed
class:
Feed Data Access¶
All the examples above produced feed data in the same order as they are defined in the persons.csv
data file. This behaviour is controlled by the Keys.FEED_DATA_ACCESS
setting which offers two options:
FeedDataAccess.RANDOM
- data is provided in random order.FeedDataAccess.SEQUENTIAL
- data is provided in the order it appears in the feed (default behaviour).
The default setting can be configured via Settings
or globally in instancio.properties
.
When SEQUENTIAL
mode is selected, consider setting Keys.FEED_DATA_END_ACTION
to define how the end of the data feed should be handled.
When extending the Feed
interface, the behaviour can be specified using the @Feed.DataAccess
annotation, which takes precedence over settings:
Additionally, the behaviour can be overridden using the builder API when creating a feed instance. This method allows overriding the value set via settings or annotations:
Similarly, when using applyFeed()
, the same method is available for specifying data access behaviour:
Feed Data End¶
This setting determines the behaviour when the end of the feed is reached while generating data. It applies only if Keys.FEED_DATA_ACCESS
is set to FeedDataAccess.SEQUENTIAL
. There are two supported values:
FeedDataEndAction.FAIL
- throws an exception when the end of the feed is reached.FeedDataEndAction.RECYCLE
- recycles the data, starting from the beginning of the feed.
You can also specify this behaviour using the builder API:
Similarly, when using the applyFeed()
method, you can specify the data end action:
Feed Data Format¶
Instancio supports data feeds in both CSV and JSON formats. Using JSON feeds requires jackson-databind
to be included in your project's dependencies (Instancio does not provide this dependency transitively):
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson-databind-version}</version>
</dependency>
By default, Instancio uses CSV
as the data format. However, you can modify this behavior using the Keys.FEED_FORMAT_TYPE
setting.
To specify the data format for a custom feed interface, you can use the @Feed.FormatType
annotation:
Alternatively, the format can be set using the builder API:
or using the applyFeed()
method:
Format Options¶
The CSV format allows for configuring various options to customise how data is interpreted:
- Delimiter Character: Specifies the character used to separate fields in the CSV data.
- Comment Prefix: Defines the prefix used to identify comment lines within the CSV data.
- Whitespace Trimming: Controls whether leading and trailing whitespace around fields should be trimmed.
These options enable defining data in a customised CSV format, as shown in the example below:
The formatOptions()
method is also available when using the applyFeed()
builder API.
Feed Tags¶
Feeds in Instancio support tagging, which allows grouping data by a specific tag key and filtering it by tag values.
For feeds that extend the Feed
interface, you can specify the tag key using the @Feed.TagKey
annotation:
With the tag key defined, you can generate a list of Person
objects, filtering for those where the gender
tag value is FEMALE
, for example:
You can also specify the tag key using the builder API, which overrides the @Feed.TagKey
annotation (if present):
In addition, Instancio provides settings (Keys.FEED_TAG_KEY
and Keys.FEED_TAG_VALUE
) to configure default tag key and value settings, which can be overridden as needed. By default, these settings are null
.
If the feed includes an 'id' column, tagging allows selecting specific records based on their 'id'.
Configuration¶
Instancio configuration is encapsulated by the Settings class, a map of keys and corresponding values. The Settings
class provides a few static methods for creating settings.
Settings static factory methods | |
---|---|
Map
or java.util.Properties
.
other
settings (a clone operation).
Settings can be overridden programmatically or through a properties file.
Info
To inspect all the keys and default values, simply: System.out.println(Settings.defaults())
Overriding Settings Programmatically¶
To override programmatically, an instance of Settings
can be passed in to the builder API:
Supplying custom settings | |
---|---|
lock()
method makes the settings instance immutable. This is an optional method call. It can be used to prevent modifications if settings are shared across multiple methods or classes.
Range settings auto-adjust
When updating range settings, such as COLLECTION_MIN_SIZE
and COLLECTION_MAX_SIZE
, the range bound is auto-adjusted if the new minimum is higher than the current maximum, and vice versa.
The Keys class defines a property key for every key object, for example:
Keys.COLLECTION_MIN_SIZE
->"collection.min.size"
Keys.STRING_ALLOW_EMPTY
->"string.allow.empty"
Using these property keys, configuration values can also be overridden using a properties file.
Overriding Settings Using a Properties File¶
Default settings can be overridden using instancio.properties
. Instancio will automatically load this file from the root of the classpath. The following listing shows all the property keys that can be configured.
*.elements.nullable
, map.keys.nullable
, map.values.nullable
specify whether Instancio can generate null
values for array/collection elements and map keys and values.
*.nullable
properties specifies whether Instancio can generate null
values for a given type.
STRICT
(default) or LENIENT
. See Selector Strictness.
subtype
are used to specify default implementations for abstract types, or map types to subtypes in general. This is the same mechanism as subtype mapping, but configured via properties.
Settings Precedence¶
Instancio layers settings on top of each other, each layer overriding the previous ones. This is done in the following order:
Settings.defaults()
- Settings from
instancio.properties
- Settings injected using
@WithSettings
annotation when usingInstancioExtension
(see Settings Injection) - Settings supplied using the builder API's withSettings(Settings) method
In the absence of any other configuration, Instancio uses defaults as returned by Settings.defaults()
. If instancio.properties
is found at the root of the classpath, it will override the defaults. Finally, settings can also be overridden at runtime using @WithSettings
annotation or withSettings(Settings) method. The latter takes precedence over everything else.
Instancio Service Provider Interface¶
The InstancioServiceProvider interface allows customising how objects are created and populated. It defines the following methods, which return null
by default and can be overridden as needed:
GeneratorProvider getGeneratorProvider()
AnnotationProcessor getAnnotationProcessor()
SetterMethodResolver getSetterMethodResolver()
TypeResolver getTypeResolver()
TypeInstantiator getTypeInstantiator()
An implementation of InstancioServiceProvider
can be registered by creating a file named org.instancio.spi.InstancioServiceProvider
under /META-INF/services/
. The file should contain the fully-qualified name of the implementation class, for example:
org.example.InstancioServiceProviderImpl
GeneratorProvider
¶
This interface allows mapping a Node
to a GeneratorSpec
:
The Generators
parameter provides access to built-in generators. This is the same class that is provided to the generate()
method.
The Node
object represents a class and/or field in the node hierarchy, for example:
Person.dateOfBirth
node will have the target classLocalDate
and thejava.lang.reflect.Field
dateOfBirth
- the collection element node of
List<Phone>
will have the target classPhone
and anull
field
Use Case¶
The main use case for implementing the GeneratorProvider
is to have generators resolved automatically. For example, the following implementation generates maximum string length based on the length
attribute of the JPA @Column
annotation.
More on JPA
From version 3.3.0
Instancio supports this functionality out-of-the-box. See the JPA section for more details.
length
attribute.
null
means a value will be generated using built-in generators.
Assuming the following entity:
Calling Instancio.create()
should produce string lengths that conform to the schema:
Using the API methods set()
, supply()
, or generate()
it is still possible to override values using the API if needed:
AnnotationProcessor
¶
Experimental API @since 4.5.0
This interface allows processing custom annotations:
It has no methods to implement. Instead, it relies on user-defined methods marked with the @AnnotationHandler
annotation. The accepted signatures for @AnnotationHandler
methods are:
@AnnotationHandler
void example(Annotation annotation, GeneratorSpec<?> spec, Node node)
@AnnotationHandler
void example(Annotation annotation, GeneratorSpec<?> spec)
The annotation
and spec
parameters can be subtypes java.lang.annotation.Annotation
and org.instancio.generator.GeneratorSpec
, respectively. The node
parameter is optional, and can be omitted if it's not needed.
Use Case¶
The main use case for implementing the AnnotationProcessor
is to customise generated values based on custom annotations. Let's assume we have the following annotations and a POJO:
@MapWithKeys
annotation specifies that a given Map
must contain given keys.
@Hex
annotation denotes that a string must be a hexadecimal value of the specified length()
.
Our Pojo
declares a Map
that should contain hexadecimal strings as values. The keys can be arbitrary strings, but the map should contain foo
and bar
. To achieve this, we can implement an AnnotationProcessor
as shown below.
@AnnotationHandler
.
Node
) is omitted as it's not needed in this example.
Instancio will use methods marked with @AnnotationHandler
to process the annotations. The first argument must be the annotation, and the second is the GeneratorSpec
applicable to the annotated type (to find the specific spec interface, see the org.instancio.generator.specs
package Javadoc or the org.instancio.generators.Generators
class).
Once the above is in place, the following snippet:
should produce output similar to:
It should be noted that the AnnotationProcessor
can only be used to customise existing generators. To define a custom Generator
for a given annotation, use a custom GeneratorProvider
.
SetterMethodResolver
¶
This interface is for providing custom resolution of setter methods from fields when Keys.ASSIGNMENT_TYPE
is set to AssignmentType.METHOD
:
Use Case¶
Out of the box, Instancio can resolve setter methods from fields assuming method names follow standard naming conventions (see Assignment Settings for details). A custom SetterMethodResolver
implementation allows tests to use AssignmentType.METHOD
with applications that follow non-standard naming conventions.
Consider the following example, where the POJO has a field prefixed with an underscore. The goal is to populate the POJO via the setter method as it contains some logic:
However, the field name _value
does not map to method name setValue()
. Therefore, the setter will not be resolved, and the value will be populated via field assignment as a fallback. A custom SetterMethodResolver
can be implemented to handle this case as shown below.
null
means built-in method resolvers will be used as a fallback.
With the above in place, the Pojo
can be created as follows:
TypeResolver
¶
This interface allows mapping a type to a subtype:
The subtype mapping uses the same mechanism as the subtype()
API method.
Use Case¶
The primary use case for implementing the TypeResolver
is to resolve subtypes automatically. By default, Instancio does not resolve the implementation class if given an abstract type. Instead, the implementation class must be specified manually. This can be done either via the subtype()
method:
or via Settings
:
Using TypeResolver
, the subtype can be resolved automatically:
Then calling Instancio.create()
should use the specified subtype:
Scanning the Classpath
Using TypeResolver
it is also possible to resolve implementation classes via classpath scanning, for example, using a third-party library. For a sample implementation, see type-resolver-sample
.
TypeInstantiator
¶
This interface is for providing custom instantiation logic for classes that Instancio is unable to instantiate out-of-the-box:
By default, Instancio attempts to instantiate a class using the default constructor. If the default constructor is unavailable or fails (for example, the constructor throws an exception), Instancio will attempt to use a constructor with the least number of parameters and pass in default values. If the last option also fails, then it resorts to JDK-specific approaches, such as using sun.misc.Unsafe
. There may be situations where all the listed options fail, which would result in null
values being generated. Using TypeInstantiator
allows plugging in custom instantiation logic.
Troubleshooting¶
Debugging¶
Instancio uses SLF4J for logging. Most of the messages are logged at DEBUG
or TRACE
level. Logging information can be useful when Instancio produces an error or does not generate expected values.
In addition to logging, the builder API provides the verbose()
method that outputs current settings as well as the internal model containing the node hierarchy to standard output. For example:
will produce (ignoring settings output for brevity):
The verbose()
method can be particularly useful when working with deep, complex class hierarchies that contain many fields, collections, and cyclic relationships. For example, some APIs such as assign()
and emit()
require that a given selector matches exactly one target. The node hierarchy can be used to troubleshoot cases where the selector happens to match more than one target. The visual representation makes it easier to fine-tune the selector by specifying selector scope or depth.
Error Handling¶
The default behaviour of Instancio is to fully populate an object, up to a certain depth. In case of internal errors, Instancio will still attempt to return an object, though some fields or collections may not be fully populated.
Consider the following somewhat contrived example:
Since it is not possible to create a Set
of 10 booleans, Instancio will generate a set of size 2, containing values true
and false
. However, internally, this use case will produce an exception that is suppressed by default. At DEBUG
log level, Instancio will report the following:
Suppressed error because Keys.FAIL_ON_ERROR (fail.on.error) is disabled.
-> To propagate the error, set Keys.FAIL_ON_ERROR setting to true.
-> To display the stack trace, run in verbose() mode or with TRACE logging.
org.instancio.exception.InstancioException: Internal error occurred creating an object.
Internal errors are suppressed by default and
can be ignored if not applicable to the current test
-> at com.example.ExampleTest(ExampleTest.java:123)
Reason: unable to populate Collection of size 10: class Set<Boolean> (depth=0)
As the message suggests, the Keys.FAIL_ON_ERROR
setting can be enabled to propagate internal errors. This can be done via the Settings
API or configuration file:
Seed¶
Before creating an object, Instancio initialises a random seed value. This seed value is used internally by the pseudorandom number generator, that is, java.util.Random
. Instancio ensures that the same instance of the random number generator is used throughout object creation, from start to finish. This means that Instancio can reproduce the same object again by using the same seed. This feature allows reproducing failed tests (see the section on reproducing tests with JUnit).
In addition, Instancio handles classes like UUID
and LocalDateTime
, where a minor difference in values can cause an object equality check to fail. These classes are generated in such a way, that for a given seed value, the generated values will be the same. To illustrate with an example, we will use the following SamplePojo
class.
By supplying the same seed value, the same object is generated:
Generating two SamplePojo instances with the same seed | |
---|---|
If the objects are printed, both produce the same output:
While the generated values are the same, it is not recommended to write assertions using hard-coded values.
Specifying Seed Value¶
By default, if no custom seed is specified, Instancio generates a random seed value. Therefore, each execution results in different outputs. This behaviour can be overridden by specifying a custom seed using any of the options below. These are ranked from highest to lowest precedence:
- withSeed(long) method of the builder API
- withSettings(Settings) or withSetting(SettingKey<T>, T) method of the builder API using
Keys.SEED
@WithSettings
annotations (requiresInstancioExtension
)@Seed
annotation (requiresInstancioExtension
)instancio.properties
file (see Global Seed for details)- random seed
Precedence rules are summarised in the following table, where each number represents a seed value, and R
represents a random seed.
Random seed | .properties | @Seed | @WithSettings | .withSettings() | .withSeed() | Actual seed |
---|---|---|---|---|---|---|
R | 5 | 4 | 3 | 2 | 1 | 1 |
R | 5 | 4 | 3 | 2 | - | 2 |
R | 5 | 4 | 3 | - | - | 3 |
R | 5 | 4 | - | - | - | 4 |
R | 5 | - | - | - | - | 5 |
R | - | - | - | - | - | R |
@WithSettings
seed¶
When a seed is specified via @WithSettings
, all objects generated within the test class are created using the given seed. For this reason, if two objects of the same type are created, both instances will be identical, for example:
This is because the above snippet is equivalent to:
Global Seed¶
A global seed can be specified in instancio.properties
using the seed
property key:
There are some important differences in how the global seed works depending on whether tests declare the InstancioExtension
.
Global Seed Without the InstancioExtension
¶
When tests are run without the extension, the same Random
instance is used across all test classes and methods. Therefore, generated data is affected by the order in which test methods are run.
Let's assume the configured seed in the properties file produces the following output if test1
is run first:
If test2
were to run first, then s2
will be FCGVRXSUU
and s1
will be OCNVRBX
.
In short, when using the global seed without the InstancioExtension
, the generated data is static for:
- a given test method, or
- a set of test methods that are run in a particular order
For this reason, using a global seed without the extension is not recommended, as it makes it harder to reproduce the data in case of test failure.
Global Seed With the InstancioExtension
¶
When using the extension, each test method gets its own instance of Random
initialised with the seed from the properties file. As a result, generated data is not affected by the order in which test methods are run.
For example, the following snippet will always produce the same output:
Getting the Seed Value¶
Sometimes it is necessary to get the seed value that was used to generate the data. One such example is for reproducing failed tests. If you are using JUnit 5, the seed value is reported automatically using the InstancioExtension
(see JUnit Jupiter integration). If you are using JUnit 4, TestNG, or Instancio standalone, the seed value can be obtained by calling the asResult()
method of the builder API. This returns a Result
containing the created object and the seed value that was used to populate its values.
Example of using asResult() | |
---|---|
In addition, seed values can be logged by setting the following class to TRACE
:
This will create a log message for every generated root object created via the API:
JUnit Jupiter Integration¶
Instancio supports JUnit 5 through the InstancioExtension, which provides several useful features:
- Injection of fields and method parameters using the
@Given
annotation - Ability to use
@InstancioSource
with@ParameterizedTest
methods - Injection of custom settings using the
@WithSettings
annotation - Support for reproducing failed tests using the
@Seed
annotation
@Given
Injection¶
Experimental API @since 5.0.0
The @Given annotation can be used to inject generated values into fields and method parameters:
Under the hood, the extension will execute Instancio.create(Person.class)
and inject the result into the annotated field.
The @Given
annotation can also be used to add parameters to @Test
methods:
Additionally, the @Given
annotation can supplement @ParameterizedTest
methods with additional arguments. In the following example, the value
parameter is provided by the @ValueSource
, while the number
is provided by Instancio:
Moreover, the @Given
annotation supports injecting Supplier
and Stream
objects:
Using Custom GivenProvider
¶
The @Given
annotation can accept one or more GivenProvider
classes to provide customised values. The GivenProvider
interface is defined as follows:
The ElementContext
class provides information about the annotated element (java.lang.reflect.Field
or java.lang.reflect.Parameter
) as well as an instance of Random
for generating reproducible random values.
For example, to generate a product code consisting of 3 letters followed by 5 digits (e.g. ABC12345
):
You can use this class with the @Given
annotation to generate product codes:
If a provider is widely used across many tests, consider defining a custom annotation:
Sample usage:
Using multiple providers
When multiple providers are specified, e.g. @Given({Provider1.class, Provider2.class})
Instancio will choose providers randomly from the specified classes.
Parameterized Tests with @InstancioSource
¶
The @InstancioSource annotation allows you to provide arguments directly to a @ParameterizedTest
method. This works for both single and multiple arguments, with each class representing one argument.
Using @ParameterizedTest
requires the junit-jupiter-params
dependency.
Using @InstancioSource with @ParameterizedTest | |
---|---|
@InstancioSource
can also be used with data feeds. For example, assuming we have a custom PersonFeed
:
Methods annotated with @InstancioSource
will execute multiple times, each time with a new set of random inputs to ensure better test coverage. The number of iterations can be configured using the Keys.INSTANCIO_SOURCE_SAMPLES
setting, which defaults to 100
. In the following example, all test methods annotated with @InstancioSource
in this test class will run 500
times:
New behaviour since version 5.0.0
. In prior versions, @InstancioSource
methods executed only once.
Additionally, this can be overridden for a specific test method using the annotation's samples
attribute:
Using @Given
with @InstancioSource
¶
Methods annotated with @InstancioSource
support the @Given
annotation. Since @InstancioSource
generates random arguments by default, the @Given
annotation is only necessary when specifying a custom GivenProvider
attribute.
In this example, randomString
is generated randomly, while customString
is provided by CustomStringProvider
.
Settings Injection¶
The InstancioExtension
also adds support for injecting Settings into a test class. The injected settings will be used by every test method within the class. This can be done using the @WithSettings annotation.
There can be only one field annotated @WithSettings
per test class.
Instancio also supports overriding the injected settings using the withSettings
method as shown below. The settings provided via the method take precedence over the injected settings (see Settings Precedence for further information).
Instancio supports @WithSettings
placed on static and non-static fields. However, if the test class contains a @ParameterizedTest
method, then the settings field must be static.
Reproducing Failed Tests¶
Since using Instancio in your tests runs your code against different inputs on each execution, the ability to reproduce failed tests with previously generated data becomes essential. Instancio supports this by reporting the seed value of a failed test in the failure message.
Data Guarantees¶
The library guarantees that identical data is generated for a given seed and version of the library. This applies to all APIs except those marked with the @NonDeterministic
annotation. An example of non-deterministic APIs are the past()
and future()
methods for generating temporal types. These methods depend on the current date as a reference point, which changes over time. Consequently, the same seed value might produce different results at different times.
Hard-coding generated values in assertions is also highly discouraged to avoid breaking changes. For example, the following test suffers from tight coupling with the random number generator implementation and may break when upgrading to a newer version of Instancio.
Seed Lifecycle in a JUnit Jupiter Test¶
Instancio initialises a seed value before each test method. This seed value is used for creating all objects during the test method's execution unless another seed is specified explicitly using the withSeed(long) method.
Seed Lifecycle in a JUnit Test | |
---|---|
8276
.
8276
.
123
.
8276
.
8276
goes out of scope.
Even though person1
and person3
are created using the same seed value of 8276
, they are actually distinct objects, each containing different values. This is because the same instance of the random number generator is used throughout the test method.
It should be noted that if the test fails, only the seed generated internally is reported (8276
in the above example). Seeds specified via withSeed()
or Settings
are not reported since the value is already known.
Test Failure Reporting¶
When a test method fails, Instancio adds a message containing the seed value to the failed test's output. Using the following failing test as an example:
Test failure example | |
---|---|
The failed test output will include the following message:
The seed source indicates whether the Instancio extension generated a random seed or used a seed provided by the user. The possible seed sources are listed below (see also Specifying seed value):
- seed specified via
Settings
annotated with@WithSettings
- seed specified using the
@Seed
annotation - random seed (default behaviour when an explicit seed is not specified)
Seeds specified using withSeed(long) or withSettings(Settings) methods are not reported by the Instancio extension.
The failed test can be reproduced by using the seed reported in the failure message. This can be done by placing the @Seed annotation on the test method:
Reproducing a failed test | |
---|---|
With the @Seed
annotation in place, the data becomes effectively static. This allows the root cause to be established and fixed. Once the test is passing, the @Seed
annotation can be removed so that new data will be generated on each subsequent test run.
Appendix¶
Default String Conversion¶
Type | Conversion Function | Sample Input |
---|---|---|
String | Function.identity() | - |
Enum | Enum::valueOf | - |
Character | s -> s.charAt(0) | "Example" -> 'E' |
Boolean | Boolean::valueOf | "true" or "false" |
Integer | Integer::valueOf | "123" |
Long | Long::valueOf | "123" |
Byte | Byte::valueOf | "123" |
Short | Short::valueOf | "123" |
Float | Float::valueOf | "123.5" |
Double | Double::valueOf | "123.5" |
BigInteger | BigInteger::new | "123" |
BigDecimal | BigDecimal::new | "123.5" |
Instant | Instant::parse | "2071-10-04T08:48:21.499609989Z" |
LocalTime | LocalTime::parse | "06:50:07.871441943" |
LocalDate | LocalDate::parse | "2048-12-24" |
LocalDateTime | LocalDateTime::parse | "2036-03-19T19:18:52.725994144" |
OffsetTime | OffsetTime::parse | "18:16:16.814320739Z" |
OffsetDateTime | OffsetDateTime::parse | "1975-12-04T07:34:43.807103492Z" |
ZonedDateTime | ZonedDateTime::parse | "2003-07-23T23:16:10.568513867Z" |
YearMonth | YearMonth::parse | "2045-05" |
Year | Year::parse | "1991" |
UUID | UUID::fromString | "5d418896-acf5-439e-902d-86a6c6fca4ae" |
Built-in Generators¶
The list of generators provided below is available via the generate()
method. Most of these generators are also available through the standalone Gen
class. Each one offers customisation options relevant to the class it generates, for example bigDecimal().scale(5)
.
Tip
See the org.instancio.generator.specs
package Javadocs for methods offered by the built-in generators below.
Generators
│
├── booleans()
├── chars()
├── bytes()
├── shorts()
├── ints()
├── longs()
├── floats()
├── doubles()
├── string()
│
├── array()
├── collection()
├── map()
├── emit()
├── enumOf(Class<E>)
├── enumSet(Class<E>)
│
├── oneOf(Collection<T>)
├── oneOf(T...)
│
├── optional()
│
├── math()
│ ├── bigInteger()
│ └── bigDecimal()
│
├── net()
│ ├── email()
│ ├── ip4()
│ ├── uri()
│ └── url()
│
├── intervalStarting(T)
│
├── io()
│ └── file()
│
├── nio()
│ └── path()
│
├── atomic()
│ ├── atomicInteger()
│ └── atomicLong()
│
├── temporal()
│ ├── calendar()
│ ├── date()
│ ├── duration()
│ ├── instant()
│ ├── localDate()
│ ├── localDateTime()
│ ├── localTime()
│ ├── offsetDateTime()
│ ├── offsetTime()
│ ├── period()
│ ├── sqlDate()
│ ├── timestamp()
│ ├── year()
│ ├── yearMonth()
│ └── zonedDateTime()
│
├── text()
│ ├── csv()
│ ├── loremIpsum()
│ ├── pattern(String)
│ ├── uuid()
│ ├── word()
│ └── wordTemplate(String)
│
├── checksum()
│ ├── luhn()
│ ├── mod10()
│ └── mod11()
│
├── finance()
│ └── creditCard()
│
├── id()
│ ├── ean()
│ ├── isbn()
│ ├── bra()
│ │ ├── cpf()
│ │ ├── cnpj()
│ │ └── tituloEleitoral()
│ ├── can()
│ │ └── sin()
│ ├── pol()
│ │ ├── nip()
│ │ ├── pesel()
│ │ └── regon()
│ ├── rus()
│ │ └── inn()
│ └── usa()
│ └── ssn()
│
├── shuffle(T...)
├── shuffle(Collection<T>)
│
└── spatial()
└── coordinate()
├── lat()
└── lon()
The io().file()
and nio().path()
generators can save files on the filesystem.