Skip to content

cuioss/cui-test-generator

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

240 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

cui-test-generator

What is it?

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.

Maven Coordinates

<dependency>
    <groupId>de.cuioss.test</groupId>
    <artifactId>cui-test-generator</artifactId>
</dependency>

Core Components

TypedGenerator - The Core Interface

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;
    }
}

Generators - The Central Factory

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);

GeneratorType Enum

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 Generators class:

    • 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

Additional Generators

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();

JUnit 5 Integration

EnableGeneratorController

The @EnableGeneratorController annotation enables reproducible test data generation:

@EnableGeneratorController
class MyGeneratorTest {
    @Test
    void shouldGenerateConsistentData() {
        var result = Generators.strings().next();
        assertFalse(result.isEmpty());
    }
}

GeneratorSeed

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
}

Parameterized Tests

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("@"));
    }
}

TypeGeneratorSource and TypeGeneratorMethodSource

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
    }
}

TypeGeneratorFactorySource

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));
    }
}

CompositeTypeGeneratorSource

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);
    }
}

Java Platform Module System (JPMS) Compatibility

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.

The Problem

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 module

The Solution

Configure 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.

Alternative Approaches

If you need to maintain full module path compliance for tests:

  1. Use @TypeGeneratorMethodSource: Define a factory method inside your test class, avoiding external class reflection entirely

  2. Test-specific module descriptor: Use an open module in src/test/java/module-info.java

  3. --add-opens JVM argument: Add --add-opens your.module/your.test.package=ALL-UNNAMED to your test configuration

Best Practices

  1. Use Generators as your primary entry point

  2. Enable @EnableGeneratorController for reproducible tests

  3. Document seeds used for specific test scenarios

  4. Create custom generators by implementing TypedGenerator

  5. Use domain-specific generators for specialized test data

  6. For modular projects, configure Surefire with useModulePath=false to avoid JPMS access restrictions

About

Provides Generator for arbitrary Java-Types

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Contributors 8

Languages