The CUI Test Generator framework provides robust and reproducible test data generation. It combines random data generation with the ability to reproduce specific test scenarios, making it ideal for both thorough testing and precise debugging.
<dependency>
<groupId>de.cuioss.test</groupId>
<artifactId>cui-test-generator</artifactId>
</dependency>TypedGenerator<T> is the foundation interface for all generators. It defines two methods: next() to generate a value and getType() to provide type information.
public class CustomGenerator implements TypedGenerator<MyType> {
@Override
public MyType next() {
return new MyType(Generators.strings().next());
}
@Override
public Class<MyType> getType() {
return MyType.class;
}
}The Generators class is your primary entry point for test data generation. It provides factory methods for creating generators for most Java built-in types:
// Basic type generation
String text = Generators.strings().next();
Integer number = Generators.integers().next();
LocalDateTime dateTime = Generators.localDateTimes().next();
// Configurable generation
String letters = Generators.letterStrings(5, 10).next(); // 5-10 characters
Integer bounded = Generators.integers(1, 100).next(); // number between 1-100
// Collection generation
List<String> strings = Generators.asCollectionGenerator(Generators.strings()).list(5);
// Fixed and enum values
var urlGen = Generators.fixedValues(String.class,
"https://cuioss.de",
"https://www.heise.de");
var enumGen = Generators.enumValues(TimeUnit.class);The GeneratorType enum provides a type-safe way to reference all available generators. It is used with @GeneratorsSource and @CompositeTypeGeneratorSource for parameterized tests.
-
Standard generators from the
Generatorsclass:-
STRINGS,INTEGERS,BOOLEANS,LOCAL_DATE_TIMES,URLS, and more
-
-
Domain-specific generators with
DOMAIN_prefix:-
DOMAIN_EMAIL,DOMAIN_CITY,DOMAIN_FULL_NAME,DOMAIN_ZIP_CODE, and more
-
The framework includes generators with static helper methods and domain-specific generators beyond the Generators factory:
// Date/Time with zones using static helper methods
var dateTime = ZonedDateTimeGenerator.any();
// Domain-specific generators (also available via GeneratorType enum)
var email = new EmailGenerator().next();
var city = new CityGenerator().next();
var name = new FullNameGenerator().next();The @EnableGeneratorController annotation enables reproducible test data generation:
@EnableGeneratorController
class MyGeneratorTest {
@Test
void shouldGenerateConsistentData() {
var result = Generators.strings().next();
assertFalse(result.isEmpty());
}
}Use @GeneratorSeed to fix the random seed for reproducible tests. It can be applied at method or class level:
@EnableGeneratorController
class MyTest {
@Test
@GeneratorSeed(4711L) // Method-level seed
void shouldGenerateSpecificData() {
// Always generates the same data
var data = Generators.strings().next();
}
}
@EnableGeneratorController
@GeneratorSeed(8042L) // Class-level seed
class AllTestsReproducible {
// All tests use the same seed
}The recommended way to use generators in parameterized tests is with @GeneratorsSource and the GeneratorType enum:
@EnableGeneratorController
class GeneratorsSourceTest {
// String generator with size parameters
@ParameterizedTest
@GeneratorsSource(
generator = GeneratorType.STRINGS,
minSize = 3,
maxSize = 10,
count = 5
)
void testWithStringGenerator(String value) {
assertNotNull(value);
assertTrue(value.length() >= 3 && value.length() <= 10);
}
// Number generator with range parameters
@ParameterizedTest
@GeneratorsSource(
generator = GeneratorType.INTEGERS,
low = "1",
high = "100",
count = 5
)
void testWithIntegerGenerator(Integer value) {
assertNotNull(value);
assertTrue(value >= 1 && value <= 100);
}
// Domain-specific generator
@ParameterizedTest
@GeneratorsSource(
generator = GeneratorType.DOMAIN_EMAIL,
count = 3
)
void testWithEmailGenerator(String email) {
assertNotNull(email);
assertTrue(email.contains("@"));
}
}Use @TypeGeneratorSource with a generator class directly, or @TypeGeneratorMethodSource with a factory method:
@EnableGeneratorController
class ParameterizedGeneratorTest {
// Class-based - uses the generator's default constructor
@ParameterizedTest
@TypeGeneratorSource(NonBlankStringGenerator.class)
void testWithGeneratedStrings(String value) {
assertNotNull(value);
assertFalse(value.isBlank());
}
// Method-based - uses a method that returns a configured generator
@ParameterizedTest
@TypeGeneratorMethodSource("createStringGenerator")
void testWithCustomGenerator(String value) {
assertNotNull(value);
}
static TypedGenerator<String> createStringGenerator() {
return Generators.strings(5, 10);
}
// Reference a method in another class
@ParameterizedTest
@TypeGeneratorMethodSource("de.cuioss.test.MyGeneratorFactory#createGenerator")
void testWithExternalGenerator(MyType value) {
// test with value
}
}Use @TypeGeneratorFactorySource to create generators using an external factory class:
@EnableGeneratorController
class FactoryBasedGeneratorTest {
@ParameterizedTest
@TypeGeneratorFactorySource(
factoryClass = MyGeneratorFactory.class,
factoryMethod = "createRangeGenerator",
methodParameters = {"1", "100"},
count = 5
)
void testWithParameterizedFactory(Integer value) {
assertNotNull(value);
assertTrue(value >= 1 && value <= 100);
}
}
public class MyGeneratorFactory {
public static TypedGenerator<String> createStringGenerator() {
return Generators.strings(5, 10);
}
public static TypedGenerator<Integer> createRangeGenerator(String min, String max) {
return Generators.integers(Integer.parseInt(min), Integer.parseInt(max));
}
}Use @CompositeTypeGeneratorSource to combine multiple generators for tests with multiple parameters:
@EnableGeneratorController
class CompositeGeneratorTest {
// Preferred: Using GeneratorType enum
@ParameterizedTest
@CompositeTypeGeneratorSource(
generators = {
GeneratorType.NON_EMPTY_STRINGS,
GeneratorType.INTEGERS
},
count = 3
)
void testWithGeneratorTypes(String text, Integer number) {
assertNotNull(text);
assertNotNull(number);
}
// Alternative: Using generator classes
@ParameterizedTest
@CompositeTypeGeneratorSource(
generatorClasses = {
NonBlankStringGenerator.class,
IntegerGenerator.class
},
count = 3
)
void testWithMultipleGenerators(String text, Integer number) {
assertNotNull(text);
assertNotNull(number);
}
}When using cui-test-generator in modular Java projects (projects with module-info.java), you may encounter access issues when test generators are located in the test source tree and referenced by @TypeGeneratorSource annotations. This happens because JPMS restricts access between modules and unnamed modules.
|
Note
|
Since version 3.0, the framework automatically detects JPMS access issues and provides actionable error messages with concrete remediation steps. |
If you have generators in src/test/java and encounter errors like:
IllegalAccessException: module your.module.name does not export your.test.generators to unnamed moduleConfigure Maven Surefire to run tests on the classpath instead of the module path by adding this to your pom.xml:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<useModulePath>false</useModulePath>
</configuration>
</plugin>This is the recommended approach as it provides the best balance of modularity and testing flexibility.
If you need to maintain full module path compliance for tests:
-
Use
@TypeGeneratorMethodSource: Define a factory method inside your test class, avoiding external class reflection entirely -
Test-specific module descriptor: Use an
open moduleinsrc/test/java/module-info.java -
--add-opensJVM argument: Add--add-opens your.module/your.test.package=ALL-UNNAMEDto your test configuration
-
Use
Generatorsas your primary entry point -
Enable
@EnableGeneratorControllerfor reproducible tests -
Document seeds used for specific test scenarios
-
Create custom generators by implementing
TypedGenerator -
Use domain-specific generators for specialized test data
-
For modular projects, configure Surefire with
useModulePath=falseto avoid JPMS access restrictions