Skip to main content
Version: 10.x

Customization

Customization is one of the most powerful features offered by AutoParams. It gives you full control over how test data is generated, allowing you to enforce business rules or tailor the data to meet specific testing needs.

For example, suppose the Product entity must follow these business rules:

  • priceAmount must be greater than or equal to 10
  • priceAmount must be less than or equal to 10000

You can implement these rules using a custom generator by extending ObjectGeneratorBase<T>:

public class ProductGenerator extends ObjectGeneratorBase<Product> {

@Override
protected Product generateObject(ObjectQuery query, ResolutionContext context) {
UUID id = context.resolve();
String name = context.resolve();

ThreadLocalRandom random = ThreadLocalRandom.current();
BigDecimal priceAmount = new BigDecimal(random.nextInt(10, 10000 + 1));

return new Product(id, name, priceAmount);
}
}

This custom generator creates a Product instance that adheres to the business constraints. It uses ResolutionContext to generate supporting values like id and name, and applies explicit logic to generate a valid priceAmount.

You can apply this custom generator using the @Customization annotation:

@Test
@AutoParams
@Customization(ProductGenerator.class)
void testMethod(Product product) {
assertTrue(product.getPriceAmount().compareTo(BigDecimal.valueOf(10)) >= 0);
assertTrue(product.getPriceAmount().compareTo(BigDecimal.valueOf(10000)) <= 0);
}

In this test, AutoParams uses ProductGenerator to ensure that the Product instance respects the required pricing constraints.

You can also apply multiple custom generators at once by listing them in the @Customization annotation:

public class ReviewGenerator extends ObjectGeneratorBase<Review> {

@Override
protected Review generateObject(ObjectQuery query, ResolutionContext context) {
UUID id = context.resolve();
UUID reviewerId = context.resolve();
Product product = context.resolve();
String comment = context.resolve();

ThreadLocalRandom random = ThreadLocalRandom.current();
int rating = random.nextInt(1, 5 + 1);

return new Review(id, reviewerId, product, rating, comment);
}
}
@Test
@AutoParams
@Customization({ ProductGenerator.class, ReviewGenerator.class })
void testMethod(Product product, Review review) {
assertTrue(product.getPriceAmount().compareTo(BigDecimal.valueOf(10)) >= 0);
assertTrue(product.getPriceAmount().compareTo(BigDecimal.valueOf(10000)) <= 0);
assertTrue(review.getRating() >= 1);
assertTrue(review.getRating() <= 5);
}

Alternatively, if you prefer to encapsulate multiple generators into a single reusable configuration, you can extend CompositeCustomizer:

public class DomainCustomizer extends CompositeCustomizer {

public DomainCustomizer() {
super(
new ProductGenerator(),
new ReviewGenerator()
);
}
}
@Test
@AutoParams
@Customization(DomainCustomizer.class)
void testMethod(Product product, Review review) {
assertTrue(product.getPriceAmount().compareTo(BigDecimal.valueOf(10)) >= 0);
assertTrue(product.getPriceAmount().compareTo(BigDecimal.valueOf(10000)) <= 0);
assertTrue(review.getRating() >= 1);
assertTrue(review.getRating() <= 5);
}

This approach gives you a clean and reusable way to manage related custom generators, improving maintainability and consistency across your test suite.

Customization Scoping

The @Customization annotation can also be applied to individual parameters within a test method. When used in this way, the specified customization will apply to that parameter and all subsequent parameters of the same type—unless explicitly overridden.

This allows for fine-grained control over how test data is generated, making it easier to set up complex, context-specific scenarios.

Here's an example of a custom generator that creates a free product (with zero prices):

public class FreeProductGenerator extends ObjectGeneratorBase<Product> {

@Override
protected Product generateObject(ObjectQuery query, ResolutionContext context) {
UUID id = context.resolve();
String name = context.resolve();

BigDecimal priceAmount = BigDecimal.ZERO;

return new Product(id, name, priceAmount);
}
}

And here’s how to apply it to a specific parameter:

@Test
@AutoParams
@Customization(DomainCustomizer.class)
void testMethod(
Product product1,
@Customization(FreeProductGenerator.class) Product product2
) {
assertTrue(product1.getPriceAmount().compareTo(BigDecimal.valueOf(10)) >= 0);
assertTrue(product1.getPriceAmount().compareTo(BigDecimal.valueOf(10000)) <= 0);

assertEquals(BigDecimal.ZERO, product2.getPriceAmount());
}

In this test, product1 is generated using the default logic from DomainCustomizer, while product2 uses the FreeProductGenerator to produce a free product. This demonstrates how per-parameter customization gives you precise control over test data generation.

One-time Customizations with DSL(Domain-Specific Language)

AutoParams allows you to define one-time customizations directly within your test method using a domain-specific language(DSL). This is useful when you want to customize test data generation in a highly localized, context-specific way—without having to create separate generator classes.

import static autoparams.customization.dsl.ArgumentCustomizationDsl.freezeArgument;

class TestClass {

@Test
@AutoParams
void testMethod(Product product, @Max(5) int rating, ResolutionContext context) {
context.customize(
freezeArgument("product").in(Review.class).to(product),
freezeArgument("rating").to(rating)
);
Review review = context.resolve();
assertSame(product, review.getProduct());
assertEquals(rating, review.getRating());
}
}

In this example, we use the freezeArgument static method from the ArgumentCustomizationDsl class to customize the behavior of the ResolutionContext. Specifically:

  • The product property in any Review instance created by the context will be set to the product parameter of the test.
  • Likewise, the rating property will be set to the rating parameter.

This approach is especially useful for quickly fixing values without defining a full custom generator or specifying customization at the test method level. It improves the readability and maintainability of localized scenarios by keeping custom logic close to the test logic.

note

The freezeArgument(String parameterName) method relies on the availability of parameter names at runtime. However, Java does not include parameter names in bytecode by default. To ensure this works correctly, you can:

  1. Use a record class, which preserves parameter names by design.
  2. Compile with the -parameters option when using javac, or -java-parameters when using kotlinc.
    If you're using Spring Boot, this option is automatically enabled when you use the Spring Boot Gradle plugin or the Spring Boot Maven plugin.
  3. Apply the @ConstructorProperties annotation to constructors to explicitly declare parameter names.
    This annotation works only for constructors and has no effect on regular methods. If you're using Lombok and the constructor is generated by a Lombok annotation such as @AllArgsConstructor, the @ConstructorProperties annotation can be automatically added by enabling the lombok.anyConstructor.addConstructorProperties = true option.
    For more details, see: https://projectlombok.org/features/constructor

Settable Properties

If a class follows the JavaBeans convention—meaning it has a no-arguments constructor and public setter methods—AutoParams can automatically populate its properties using the InstancePropertyWriter customizer.

Here's a simple example:

@Getter
@Setter
public class User {

private Long id;
private String name;
}

By applying the InstancePropertyWriter customizer, AutoParams will generate and assign values to the id and name properties automatically:

@Test
@AutoParams
@Customization(InstancePropertyWriter.class)
void testMethod(User user) {
assertNotNull(user.getId());
assertNotNull(user.getName());
}

In this test, the User object is created using its default constructor, and AutoParams sets values on its writable properties via their setters. This is especially useful when working with legacy models or data transfer objects(DTOs) that do not have constructors covering all fields.