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 about the actual values. 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 in order 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.
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 API¶
This section provides an overview of the API for creating and customising objects.
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 the Instancio.of(Class)
method and specifying the type parameters manually:
However, this approach has a couple of drawbacks: it does not supported nested generics, and its usage will generate an "unchecked assignment" warning.
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:
Shorthand methods | |
---|---|
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.
Examples of stream() methods | |
---|---|
Since returned streams are infinite, limit()
must be called to avoid an infinite loop.
Selectors¶
Selectors are used to target fields and classes, for example in order to customise generated values. Selectors are provided by the Select class which contains the following methods:
Static methods for targeting fields and classes | |
---|---|
all(String.class)
all(all(int.class), all(Integer.class))
The allXxx()
methods such as allInts()
, are available for all core types.
The above methods return either an instance of Selector or SelectorGroup type. The latter is a container combining multiple Selectors. For example, to ignore certain values, we can specify them individually as follows:
Examples of using selectors | |
---|---|
or alternatively, we can combine the selectors into a single group:
Examples of using a selector group | |
---|---|
Selector Precedence¶
Field selectors have higher precedence than class selectors. Consider the following example:
Selector precedence example | |
---|---|
This will produce a person object with all strings set to "foo". However, since field selectors have higher precedence, person's name will be set to "bar".
Selector Scopes¶
Selectors also offer the within(Scope... scopes)
method for fine-tuning which targets they should be applied to.
The method accepts one or more Scope
objects that can be creating using the static methods in the Select
class.
Static methods for specifying selector scopes | |
---|---|
To illustrate how scopes work we will assume the following structure for the Person
class.
Person class structure¶
Sample POJOs with getters and setters omitted | |
---|---|
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. For brevity,
the Instancio.of(Person.class)
line will be omitted.
set(allStrings().within(scope(Address.class)), "foo")
set(allStrings().within(scope(List.class)), "foo")
set(allStrings().within(scope(Person.class, "home")), "foo")
set(field(Address.class, "city").within(scope(Person.class, "home")), "foo")
Using within()
also allows specifying multiple scopes. Scopes must be specified top-down, starting from the outermost to the innermost.
set(allStrings().within(scope(Person.class, "work"), scope(Phone.class)), "foo")
The Person.work
address object contains a list of phones, therefore Person.work
is the outermost scope and is specified first.
Phone
class is the innermost scope and is specified last.
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:
If we want to create the POJO with a set of size 10, it might be tempting to do the following:
SamplePojo pojo = Instancio.of(SamplePojo.class)
.generate(all(Set.class), gen -> gen.collection().size(10))
.create();
This, however, will not work. The field is declared as a SortedSet
, but the selector is for Set
.
For the selector to match, the target class must be the same as the field's:
SamplePojo pojo = Instancio.of(SamplePojo.class)
.generate(all(SortedSet.class), gen -> gen.collection().size(10))
.create();
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 above. It gets trickier when generating more complex classes. Strict mode helps reduce this type of error.
Consider another example where the problem may not be immediately obvious:
Person person = Instancio.of(Person.class)
.ignore(all(Address.class))
.generate(field(Phone.class, "number"), gen -> gen.text().pattern("#d#d#d-#d#d-#d#d"))
.create();
Since List<Phone>
is contained within the Address
class, the generate()
method is redundant
(see Person
class structure outlined at the beginning of this section).
All addresses will be null, therefore no phone instances will be created.
Strict mode will trigger an error highlighting this problem.
Simplify fixing tests after refactoring¶
Somewhat related to the above is refactoring. Refactoring always causes test failures to some degree. Classes get reorganised and tests need to be updated to reflect those 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 concise makes them easier to maintain.
Lenient Mode¶
While strict mode is highly recommended, there is an option to switch to lenient mode.
The lenient mode can be enabled using the lenient()
method:
Person person = Instancio.of(Person.class)
.lenient()
// snip...
.create();
Lenient mode can also be enabled via `Settings`. In fact, the `lenient()` method above is a shorthand for the following:
``` java title="Setting lenient mode using Settings"
Settings settings = Settings.create()
.set(Keys.MODE, Mode.LENIENT);
Person person = Instancio.of(Person.class)
.withSettings(settings)
// snip...
.create();
Lenient mode can also be enabled globally using instancio.properties
:
Customising Objects¶
Properties of an object created by Instancio can be customised using
generate()
set()
supply()
methods defined in the InstancioApi class.
Using generate()
¶
The generate()
method provides access to built-in generators for core types from the JDK, such 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() | |
---|---|
Each generator provides methods applicable to the type it generates, for example:
gen.string().minLength(3).allowEmpty()
gen.collection().size(5).nullableElements()
gen.temporal().localDate().future()
gen.longs().min(Long.MIN_VALUE)
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:
Using set()
¶
The set()
method is self-explanatory.
It can be used to set a static value to selected fields or classes, for example:
Example of using set() | |
---|---|
countryCode
to "+1" on all generated instances of Phone
class.
LocalDateTime
values to now
.
Using supply()
¶
The supply()
method has two variants:
java.util.function.Supplier
.
Using supply() to provide non-random values¶
The first variant can be used where random values are not appropriate and the generated object needs to have a meaningful state.
Example | |
---|---|
countryCode
to "+1" for all instances of Phone
.
LocalDateTime
instances will be distinct objects with the value now()
.
There is some overlap between the set()
and supply()
methods.
For instance, the following two lines will produce identical results:
Example | |
---|---|
In fact, set()
is just a convenience method to avoid using supply()
when the value is constant.
However, the supply()
method can be used to provide a new instance each time it is called.
For example, the following methods are not identical:
Example | |
---|---|
If the Person
class has multiple LocalDateTime
fields, using set()
will set them all to the same instance, while using supply()
will set them all to distinct instances.
This difference is even more important if supplying a Collection
, since sharing a collection instance among multiple objects is usually not desired.
Using supply() to provide random values¶
The second variant of the supply()
method can be used to generate random 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 Instancio will be able to reproduce the generated object when needed.
The Random implementation uses a java.util.Random
internally, but offers a more user-friendly interface and convenience methods not available in the JDK class.
Creating a custom Generator | |
---|---|
The custom PhoneGenerator
can now be passed into the supply()
method:
Instancio also offers a Service Provider Interface, GeneratorProvider that can be used to register custom generators.
This removes the need for manually passing custom generators to the supply
method as in the above example.
They will be picked up automatically.
supply()
anti-pattern¶
Since the supply()
method provides an instance of Random, the method can also be used for customising values of core type, such as strings and numbers.
However, the generate()
method should be preferred in such cases if possible as it provides a better abstraction and would result in more readable code.
generate() vs supply() | |
---|---|
random
to generate a String
.
Using onComplete()
¶
Generated objects can also be customised using the OnCompleteCallback, a functional interface with the following signature:
While the supply()
and generate()
methods allow specifying values during object construction, the OnCompleteCallback
is used to modify the generated object after it has been fully populated.
The following example shows how the Address
can be modified using a callback.
If the 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 | |
---|---|
The advantage of callbacks is that they can be used to update multiple fields at once. The disadvantage, however, is that they can only be used to update mutable types.
It should also be noted that callbacks are only invoked on non-null values.
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 invoked.
Callbacks only called on non-null values | |
---|---|
Ignoring Fields or Classes¶
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 | |
---|---|
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.
ignore() has higher precedence than other methods | |
---|---|
Nullable Values¶
By default, Instancio generates non-null values for all fields.
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 way to specify that values can be nullable.
This can be done using:
withNullable
method of the builder API- generator methods (if a generator supports it)
- Settings
To specify that something is nullable using the builder API can be done as follows:
Example: specifying nullability using the builder API | |
---|---|
Some built-in generators also support marking values as nullable. In addition, Collection, Map, and Array generators allow specifying whether elements, keys or values are 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, nullability can be specified using Settings, but only for core types, such as strings and numbers:
Example: specifying nullability using Settings | |
---|---|
Subtype Mapping¶
Subtype mapping allows mapping a particular type to its subtype.
This can be useful for specifying a specific implementation for an abstract type.
The mapping can be specified using the subtype
method:
All the types represented by the selectors must be supertypes of the given subtype
parameter.
Example: subtype mapping | |
---|---|
Pet
is an abstract type, then without the mapping all Pet
instances will be null
since Instancio would not be able to resolve the implementation class.
Person
has an Address
field, where Address
is the superclass of AddressImpl
.
Using Models¶
A Model is a template for creating objects which encapsulates all the generation parameters specified using the builder API. For example, the following model of the Simpson's household can be used to create individual Simpson characters.
The Model
class does not expose any public methods, and its instances are effectively immutable.
However, a model can be used as template for creating other models.
The next example creates a new model that includes a new Pet
:
Example: using a model as a template for creating other models | |
---|---|
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 takes care in generating values for 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, Instancio uses a random seed to generate an object. This behaviour can be overridden using any of the following options:
instancio.properties
file@Seed
and@WithSettings
annotations (when usingInstancioExtension
for JUnit Jupiter)Settings
class- withSeed(int seed) method of the builder API
These are ranked from lowest precedence to highest. Seed value passed to withSeed()
takes precedence over other values, such as those supplied through properties or @Seed
annotation.
Seed value specified through properties is a "global" seed. All objects created by Instancio will use this seed (unless the seed is overridden using one of the other methods). This will result in the same data being generated on each run.
Getting 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, 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() | |
---|---|
Metamodel¶
This section expands on the Selectors section, which described how to target fields. Instancio uses reflection at field level to populate objects. The main reason for using fields and not setters is type erasure. It is not possible to determine the generic type of method parameters at runtime. However, generic type information is available at field level. In other words:
List<String>
.
List
.
Without knowing the list's generic type, Instancio would not be able to populate the list. For this reason, it operates at field level. Using fields, however, has one drawback: they require the use of field names. To circumvent this problem, Instancio includes an annotation processor that can generate metamodel classes.
The following example shows two selectors for the city
field of Address
, one referencing the field by name, and the other using the generated metamodel class:
_
is used as the metamodel class suffix, but this can be customised using -Ainstancio.suffix
argument.
Configuring the Annotation Processor¶
Maven¶
To configure the annotation processor with Maven, add the <annotationProcessorPaths>
element to the build plugins section in your pom.xml
as shown below.
Note
You still need to have the Instancio library, either instancio-core
or instancio-junit
, in your <dependencies>
(see Getting Started).
Gradle¶
The following can be used with Gradle version 4.6 or higher, add the following to your build.gradle
file:
Generating Metamodels¶
With the annotation processor build configuration in place, metamodels can be generated using the @InstancioMetamodel annotation. The annotation can be placed on any type, including an interface as shown below.
Using @InstancioMetamodel | |
---|---|
It is not recommended declaring the @InstancioMetamodel
annotation with the same classes
more than once.
Doing so will result in metamodels being generated more than once as well.
For this reason, it is better to have a dedicated class containing the @InstancioMetamodel
annotation.
Metamodels for classes specified in the annotation will be automatically generated during the build.
Typically metamodels are placed under a generated sources directory, such as generated/sources
or generated-sources
.
If your IDE does not pick up the generated classes, then adding the generated sources directory to the build path
(or simply reloading the project) should resolve this.
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
,
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
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.
JUnit Jupiter Integration¶
Instancio supports JUnit 5 via the InstancioExtension and can be used in combination with extensions from other testing frameworks. The extension adds a few useful features, such as
- the ability to use @InstancioSource with
@ParameterizedTest
methods, - injection of custom settings using @WithSettings,
- and most importantly support for reproducing failed tests using the @Seed annotation.
Reproducing Failed Tests¶
Since using Instancio validates your code against random inputs on each test run, having the ability to reproduce a failed tests with previously generated data becomes a necessity.
Instancio supports this use case by reporting the seed value of a failed test in the failure message using JUnit's publishReportEntry
mechanism.
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(int seed) method.
Seed Lifecycle in a JUnit Test | |
---|---|
8276
.
8276
.
123
.
8276
.
8276
goes out of scope.
Note
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 througout the test method.
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 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.
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.
Arguments Source¶
Using the @InstancioSource annotation it is possible to have arguments provided directly to a @ParameterzedTest
test method.
This works with a single argument and multiple arguments, each class representing one argument.
Using @ParameterizedTest
requires the junit-jupiter-params
dependency.
Using @InstancioSource with @ParameterizedTest | |
---|---|
It should be noted that using @InstancioSource
has a couple of important limitations that makes it unsuitable in many situations.
The biggest limitation is that the generated objects cannot be customised. The only option is to customise generated values using settings injection. However, it is not possible to customise values on a per-field basis, as you would with the builder API.
The second limitation is that it does not support parameterized types.
For instance, it is not possible to specify that @InstancioSource(List.class)
should be of type List<String>
.