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.
<dependency>
<groupId>com.github.nylle</groupId>
<artifactId>javafixture</artifactId>
<version>2.11.0</version>
<scope>test</scope>
</dependency>
Via constructor:
var fixture = new Fixture();
...or using a static factory method for convenience:
var fixture = Fixture.fixture();
String result = fixture.create(String.class);
String: "c3932f6f-59ae-43df-8ee9-8788474a3f87"
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;
}
int result = fixture.create(int.class);
int: -1612385443
ParentDto result = fixture.create(ParentDto.class);
- 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"
List<String> result = fixture.createMany(String.class).collect(Collectors.toList());
ArrayList:
- String: "333af3f6-4ed1-4580-9cae-aaee271d7ba7"
- String: "9452541b-c6f9-4316-b254-28d00b327d0d"
- String: "4ed0f3c4-5ea3-4dbb-b31c-f92c036af463"
List<String> result = fixture.build(String.class).createMany(4).collect(Collectors.toList());
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"
List<String> result = new ArrayList<>();
result.add("HELLO!");
fixture.addManyTo(result, String.class);
ArrayList:
- String: "HELLO!"
- String: "333af3f6-4ed1-4580-9cae-aaee271d7ba7"
- String: "9452541b-c6f9-4316-b254-28d00b327d0d"
- String: "4ed0f3c4-5ea3-4dbb-b31c-f92c036af463"
TestDto result = fixture.build(TestDto.class)
.with(x -> x.setMyPrivateField("HELLO!"))
.with(x -> x.myPublicField = 123)
.create();
TestDto:
- myPrivateField: String: "HELLO!"
- myPublicField: int: 123
TestDto result = fixture.build(TestDto.class)
.with("myPrivateField", "HELLO!")
.create();
TestDto:
- myPrivateField: String: "HELLO!"
- myPublicField: int: 26123854
ParentDto result = fixture.build(ParentDto.class)
.with(String.class, "hello")
.create();
- ParentDto:
- id: String: "hello"
- child: ChildDto:
- id: String: "hello"
- names: ArrayList:
- String: "hello"
- String: "hello"
- String: "hello"
TestDto result = fixture.build(TestDto.class)
.without("myPrivateField")
.without("myPublicField")
.create();
TestDto:
- myPrivateField: String: null
- myPublicField: int: 0
Primitives will receive their default-value, classes will be null
.
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); // does not compile
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); // convenience method for above
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.
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 usingStream<T> Fixture.createMany(Class<T>)
usePositiveNumbersOnly
defines whether to generate only positive numbers including 0 forshort
,int
,long
,float
, anddouble
clock
sets the clock to use when creating time-based values
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>
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);
}
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 likeint
,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 typeString
, whileint2 = 3
can only be applied to the second argument which obviously must be of typeint
and so on. - The current implementation only supports up to 6 arguments per test method.
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!