JavaFixture
JavaFixture is the attempt to bring the incredibly easy usage of Mark Seemann's AutoFixture for .NET to the Java world.
The purpose of this project is to generate full object graphs for use in test suites.
Contents
Getting Started
<dependency>
<groupId>com.github.nylle</groupId>
<artifactId>javafixture</artifactId>
<version>2.11.0</version>
<scope>test</scope>
</dependency>
Usage
Create a Fixture
Via constructor:
var fixture = new Fixture();
...or using a static factory method for convenience:
var fixture = Fixture.fixture();
Autogenerated String
String result = fixture.create(String.class);
Sample Result
String: "c3932f6f-59ae-43df-8ee9-8788474a3f87"
Note
If a String
-field is annotated with @Size
or @Column
, those limits will be respected:
public class TestObjectWithJavaxValidationAnnotations {
@Size(min = 3, max = 6)
private String withMinMaxAnnotation;
@Column(length = 6)
private String withColumnLengthAnnotation;
}
Autogenerated Number
int result = fixture.create(int.class);
Sample Result
int: -1612385443
Complex Type
ParentDto result = fixture.create(ParentDto.class);
Sample Result
- ParentDto:
- id: String: "4ed0f3c4-5ea3-4dbb-b31c-f92c036af463"
- child: ChildDto:
- id: String: "c3932f6f-59ae-43df-8ee9-8788474a3f87"
- names: ArrayList:
- String: "9452541b-c6f9-4316-b254-28d00b327d0d"
- String: "52ac46e4-1b21-40c8-9213-31fc839fbdf7"
- String: "333af3f6-4ed1-4580-9cae-aaee271d7ba7"
Collection of Strings
List<String> result = fixture.createMany(String.class).collect(Collectors.toList());
Sample Result
ArrayList:
- String: "333af3f6-4ed1-4580-9cae-aaee271d7ba7"
- String: "9452541b-c6f9-4316-b254-28d00b327d0d"
- String: "4ed0f3c4-5ea3-4dbb-b31c-f92c036af463"
Collection of Strings with Desired Size
List<String> result = fixture.build(String.class).createMany(4).collect(Collectors.toList());
Sample Result
ArrayList:
- String: "333af3f6-4ed1-4580-9cae-aaee271d7ba7"
- String: "9452541b-c6f9-4316-b254-28d00b327d0d"
- String: "4ed0f3c4-5ea3-4dbb-b31c-f92c036af463"
- String: "52ac46e4-1b21-40c8-9213-31fc839fbdf7"
Add to Collection
List<String> result = new ArrayList<>();
result.add("HELLO!");
fixture.addManyTo(result, String.class);
Sample Result
ArrayList:
- String: "HELLO!"
- String: "333af3f6-4ed1-4580-9cae-aaee271d7ba7"
- String: "9452541b-c6f9-4316-b254-28d00b327d0d"
- String: "4ed0f3c4-5ea3-4dbb-b31c-f92c036af463"
Customize Property With Lambda
TestDto result = fixture.build(TestDto.class)
.with(x -> x.setMyPrivateField("HELLO!"))
.with(x -> x.myPublicField = 123)
.create();
Sample Result
TestDto:
- myPrivateField: String: "HELLO!"
- myPublicField: int: 123
Set Private Field Using Reflection
TestDto result = fixture.build(TestDto.class)
.with("myPrivateField", "HELLO!")
.create();
Sample Result
TestDto:
- myPrivateField: String: "HELLO!"
- myPublicField: int: 26123854
Set all fields for type
ParentDto result = fixture.build(ParentDto.class)
.with(String.class, "hello")
.create();
Sample Result
- ParentDto:
- id: String: "hello"
- child: ChildDto:
- id: String: "hello"
- names: ArrayList:
- String: "hello"
- String: "hello"
- String: "hello"
Omit Field
TestDto result = fixture.build(TestDto.class)
.without("myPrivateField")
.without("myPublicField")
.create();
Sample Result
TestDto:
- myPrivateField: String: null
- myPublicField: int: 0
Note
Primitives will receive their default-value, classes will be null
.
Generics
Due to Java's type erasure (further reading: baeldung), it is difficult to reflect generic classes on runtime and the following doesn't work:
fixture.create(Optional<String>.class);
Using JavaFixture however it can be achieved through a little trick:
Optional<String> result = fixture.create(new SpecimenType<Optional<String>>(){});
Please note the empty curly braces ({}
) after the call to the constructor of SpecimenType
. These are necessary for generic reflection through an abstract superclass.
SpecimenType
can also be used for non-generic classes, but will lose any parametrisation for generic classes:
Optional result = fixture.create(SpecimenType.fromClass(Optional.class));
Optional result = fixture.create(Optional.class);
Constructor
There might be some cases when you want to create an object not by instantiating it and setting all fields, but by calling one of its constructors and feeding it random values.
var result = fixture.construct(new SpecimenType<MyGeneric<String>>(){});
var result = fixture.construct(String.class);
Keep in mind that only public constructors are allowed.
Configuration
The values below are the default values, used when no configuration is provided.
var config = Configuration.configure()
.collectionSizeRange(2, 10)
.streamSize(3)
.usePositiveNumbersOnly(false)
.clock(Clock.fixed(Instant.now(), ZoneOffset.UTC));
var fixture = new Fixture(config);
The fixture can also be configured fluently inline:
var configuredFixture = Fixture.configuration()
.collectionSizeRange(2, 10)
.streamSize(3)
.usePositiveNumbersOnly(false)
.clock(Clock.fixed(Instant.now(), ZoneOffset.UTC))
.fixture();
collectionSizeRange
determines the range from which a random collection size will be picked when creating any collection, map or arraystreamSize
determines the number of objects to be returned when using Stream<T> Fixture.createMany(Class<T>)
usePositiveNumbersOnly
defines whether to generate only positive numbers including 0 for short
, int
, long
, float
, and double
clock
sets the clock to use when creating time-based values
JUnit5 Support
In order to use JUnit5 support you need to add the following dependencies if not already present.
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.5.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.5.1</version>
<scope>test</scope>
</dependency>
Remember to also include the vintage
dependency if you still have JUnit4-tests, otherwise they won't be run.
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<version>5.5.1</version>
<scope>test</scope>
</dependency>
Inject Random Values Into Single Test
All arguments of the test-method below will be provided as a random object generated by JavaFixture.
@TestWithFixture
void injectParameterViaMethodExtension(TestDto testObject, int intValue, Optional<String> genericObject) {
assertThat(testObject).isInstanceOf(TestDto.class);
assertThat(intValue).isBetween(Integer.MIN_VALUE, Integer.MAX_VALUE);
assertThat(genericObject).isInstanceOf(Optional.class);
assertThat(genericObject).isPresent();
assertThat(genericObject.get()).isInstanceOf(String.class);
}
Additional annotated arguments are allowed:
@TestWithFixture
@DisplayName("Annotated parameters will work when they are at the end of the list")
void injectTempDirViaJunit(Integer intValue, @TempDir Path injectedTempDir) {
assertThat(injectedTempDir).isEqualTo(tempPath);
assertThat(intValue).isNotNull();
}
You can also configure Fixture inline:
@TestWithFixture(minCollectionSize = 11, maxCollectionSize = 11, positiveNumbersOnly = true)
void configurableFixture(List<Integer> input) {
assertThat(input).hasSize(11);
assertThat(input).allMatch(x -> x > 1);
}
Parameterized Tests
For some syntactic sugar, this library comes with a wrapper for JUnit5's parameterized
test feature, called @TestWithCases
.
@TestWithCases
@TestCase(str1 = "", int2 = 0)
@TestCase(str1 = " ", int2 = 1)
@TestCase(str1 = "foo", int2 = 3)
@TestCase(str1 = "hello", int2 = 5)
void testStringLength(String input, int expected) {
assertThat(input.length()).isEqualTo(expected);
}
The test will be run for every @TestCase
-annotation injecting the provided values into the test's arguments.
Due to Java's limited annotation design, the following rules apply:
- Values can only be of type
String
, Class
or primitive like int
, boolean
, float
,
etc. - Annotation parameters are indexed and they must fit to the test method argument.
Example:
str1 = "foo"
can only be applied to the first argument which must be of type
String
, while int2 = 3
can only be applied to the second argument which obviously must
be of type int
and so on. - The current implementation only supports up to 6 arguments per test method.
Parameterized Tests With Random Injected Values
If you are using @TestWithFixture
and want to make the test parameterized, you can do so by using the annotation @Fixture
inline:
@TestWithCases
@TestCase(str1 = "foo")
@TestCase(str1 = "bar")
void testStringLength(String input, @Fixture TestDto fixture) {
assertThat(input).hasSize(3);
assertThat(fixture).isNotNull();
}
The test will be run for every @TestCase
-annotation injecting the provided and the randomly generated values into the test's arguments.
The random values will be identical for all test-cases!