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 to10
priceAmount
must 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
@AutoParams
@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
@AutoParams
@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
@AutoParams
@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
@AutoParams
@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
@AutoParams
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
product
property in anyReview
instance created by the context will be set to theproduct
parameter of the test. - Likewise, the
rating
property will be set to therating
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.
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
-parameters
option when usingjavac
, or-java-parameters
when 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
@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 thelombok.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:
- 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
@AutoParams
@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.