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:
priceAmountmust be greater than or equal to10priceAmountmust be less than or equal to10000
You can implement these rules using a custom generator by extending ObjectGeneratorBase<T>:
- Java
- Kotlin
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);
}
}
class ProductGenerator : ObjectGeneratorBase<Product>() {
override fun generateObject(query: ObjectQuery, context: ResolutionContext): Product {
val id = context.resolve<UUID>()
val name = context.resolve<String>()
val random = ThreadLocalRandom.current()
val priceAmount = BigDecimal(random.nextInt(10, 10000 + 1))
return 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:
- Java
- Kotlin
@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);
}
@Test
@AutoKotlinParams
@Customization(ProductGenerator::class)
fun testMethod(product: Product) {
assertTrue(product.priceAmount >= BigDecimal.valueOf(10))
assertTrue(product.priceAmount <= BigDecimal.valueOf(10000))
}
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:
- Java
- Kotlin
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);
}
}
class ReviewGenerator : ObjectGeneratorBase<Review>() {
override fun generateObject(query: ObjectQuery, context: ResolutionContext): Review {
val id = context.resolve<UUID>()
val reviewerId = context.resolve<UUID>()
val product = context.resolve<Product>()
val comment = context.resolve<String>()
val random = ThreadLocalRandom.current()
val rating = random.nextInt(1, 5 + 1)
return Review(id, reviewerId, product, rating, comment)
}
}
- Java
- Kotlin
@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);
}
@Test
@AutoKotlinParams
@Customization(ProductGenerator::class, ReviewGenerator::class)
fun testMethod(product: Product, review: Review) {
assertTrue(product.priceAmount >= BigDecimal.valueOf(10))
assertTrue(product.priceAmount <= BigDecimal.valueOf(10000))
assertTrue(review.rating >= 1)
assertTrue(review.rating <= 5)
}
Alternatively, if you prefer to encapsulate multiple generators into a single reusable configuration, you can extend CompositeCustomizer:
- Java
- Kotlin
public class DomainCustomizer extends CompositeCustomizer {
public DomainCustomizer() {
super(
new ProductGenerator(),
new ReviewGenerator()
);
}
}
class DomainCustomizer : CompositeCustomizer(
ProductGenerator(),
ReviewGenerator()
)
- Java
- Kotlin
@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);
}
@Test
@AutoKotlinParams
@Customization(DomainCustomizer::class)
fun testMethod(product: Product, review: Review) {
assertTrue(product.priceAmount >= BigDecimal.valueOf(10))
assertTrue(product.priceAmount <= BigDecimal.valueOf(10000))
assertTrue(review.rating >= 1)
assertTrue(review.rating <= 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):
- Java
- Kotlin
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);
}
}
class FreeProductGenerator : ObjectGeneratorBase<Product>() {
override fun generateObject(query: ObjectQuery, context: ResolutionContext): Product {
val id = context.resolve<UUID>()
val name = context.resolve<String>()
val priceAmount = BigDecimal.ZERO
return Product(id, name, priceAmount)
}
}
And here’s how to apply it to a specific parameter:
- Java
- Kotlin
@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());
}
@Test
@AutoKotlinParams
@Customization(DomainCustomizer::class)
fun testMethod(
product1: Product,
@Customization(FreeProductGenerator::class) product2: Product
) {
assertTrue(product1.priceAmount >= BigDecimal.valueOf(10))
assertTrue(product1.priceAmount <= BigDecimal.valueOf(10000))
assertEquals(BigDecimal.ZERO, product2.priceAmount)
}
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.
- Java
- Kotlin
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());
}
}
import autoparams.customization.dsl.ArgumentCustomizationDsl.freezeArgument
class TestClass {
@Test
@AutoKotlinParams
fun testMethod(product: Product, @Max(5) rating: Int, context: ResolutionContext) {
context.customize(
freezeArgument("product").`in`(Review::class.java).to(product),
freezeArgument("rating").to(rating)
)
val review = context.resolve<Review>()
assertSame(product, review.product)
assertEquals(rating, review.rating)
}
}
In this example, we use the freezeArgument static method from the ArgumentCustomizationDsl class to customize the behavior of the ResolutionContext. Specifically:
- The
productproperty in anyReviewinstance created by the context will be set to theproductparameter of the test. - Likewise, the
ratingproperty will be set to theratingparameter.
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.
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:
- Use a record class, which preserves parameter names by design.
- Compile with the
-parametersoption when usingjavac, or-java-parameterswhen usingkotlinc.
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. - Apply the
@ConstructorPropertiesannotation 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@ConstructorPropertiesannotation can be automatically added by enabling thelombok.anyConstructor.addConstructorProperties = trueoption.
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:
- Java
- Kotlin
@Getter
@Setter
public class User {
private Long id;
private String name;
}
data class User(
var id: Long? = null,
var name: String? = null
)
By applying the InstancePropertyWriter customizer, AutoParams will generate and assign values to the id and name properties automatically:
- Java
- Kotlin
@Test
@AutoParams
@Customization(InstancePropertyWriter.class)
void testMethod(User user) {
assertNotNull(user.getId());
assertNotNull(user.getName());
}
@Test
@AutoKotlinParams
@Customization(InstancePropertyWriter::class)
fun testMethod(user: User) {
assertNotNull(user.id)
assertNotNull(user.name)
}
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.