Creating Custom Introspector
Note: This guide is for advanced users who already understand Fixture Monkey basics. Most users won’t need to create custom introspectors as the built-in ones handle common scenarios. If you’re just getting started, check the Introspector guide first.
When Would You Need a Custom Introspector?
You might need to create a custom introspector in these specific situations:
- Your classes have unique creation requirements that built-in introspectors can’t handle
- You’re working with third-party libraries that follow unconventional patterns
- Your objects need special initialization logic that can’t be addressed with the
instantiate
method
If none of these apply to you, the built-in introspectors will likely be sufficient.
Basic Approaches
There are two main ways to create custom introspectors:
1. Assembling Objects from Properties
This approach is useful when you need to manually construct objects using their properties:
// Example custom introspector that handles a specific class type
public class CustomArbitraryIntrospector implements ArbitraryIntrospector {
@Override
public ArbitraryIntrospectorResult introspect(ArbitraryGeneratorContext context) {
// Step 1: Check if this introspector should handle this type
Class<?> type = context.getResolvedType().getType();
if (!MyCustomClass.class.isAssignableFrom(type)) {
// If not our target type, let other introspectors handle it
return ArbitraryIntrospectorResult.NOT_INTROSPECTED;
}
// Step 2: Get the properties Fixture Monkey generated for this object
Map<ArbitraryProperty, CombinableArbitrary<?>> arbitrariesByProperty =
context.getCombinableArbitrariesByArbitraryProperty();
// Step 3: Build the object using these properties
CombinableArbitrary<MyCustomClass> combinableArbitrary = CombinableArbitrary.objectBuilder()
.properties(arbitrariesByProperty)
.build(propertyValues -> {
// Create a new instance of our class
MyCustomClass obj = new MyCustomClass();
// Set each property value
propertyValues.forEach((property, value) -> {
String propertyName = property.getName();
if ("name".equals(propertyName)) {
obj.setName((String) value);
} else if ("value".equals(propertyName)) {
obj.setValue((Integer) value);
}
});
return obj;
});
// Step 4: Return the result
return new ArbitraryIntrospectorResult(combinableArbitrary);
}
}
1.1 Defining Required Properties
Sometimes you need to customize how child properties are discovered and generated:
// Override this method to control property generation for specific properties
@Override
@Nullable
public PropertyGenerator getRequiredPropertyGenerator(Property property) {
// Check if this property needs special handling
if ("nestedObject".equals(property.getName())) {
// Create a generator that only includes specific fields
return new FieldPropertyGenerator(
// Only include id and name fields
field -> "id".equals(field.getName()) || "name".equals(field.getName()),
// Match all fields that pass the filter
field -> true
);
}
// For other properties, use the default generator
return null;
}
2. Returning Fixed Instances
Sometimes you just need to return a constant or specially calculated value:
// Example introspector that returns a fixed value for a specific type
public class ConstantArbitraryIntrospector implements ArbitraryIntrospector {
private final Object constantValue;
public ConstantArbitraryIntrospector(Object constantValue) {
this.constantValue = constantValue;
}
@Override
public ArbitraryIntrospectorResult introspect(ArbitraryGeneratorContext context) {
Class<?> type = context.getResolvedType().getType();
// Make sure our constant is the right type
if (!type.isInstance(constantValue)) {
return ArbitraryIntrospectorResult.NOT_INTROSPECTED;
}
// Return our constant value
return new ArbitraryIntrospectorResult(
CombinableArbitrary.from(constantValue)
);
}
}
Using Your Custom Introspector
After creating your introspector, you can use it in two ways:
As the Primary Introspector
// Create your custom introspector
ArbitraryIntrospector customIntrospector = new CustomArbitraryIntrospector();
// Use it as the main introspector
FixtureMonkey fixtureMonkey = FixtureMonkey.builder()
.objectIntrospector(customIntrospector)
.build();
// Generate objects
MyCustomClass obj = fixtureMonkey.giveMeOne(MyCustomClass.class);
Combined with Other Introspectors
Usually, you’ll want to combine your custom introspector with the built-in ones:
// Create a Fixture Monkey that tries your introspector first,
// then falls back to the standard ones if yours doesn't apply
FixtureMonkey fixtureMonkey = FixtureMonkey.builder()
.objectIntrospector(new FailoverIntrospector(
Arrays.asList(
customIntrospector, // Try your custom one first
ConstructorPropertiesArbitraryIntrospector.INSTANCE,
BuilderArbitraryIntrospector.INSTANCE,
FieldReflectionArbitraryIntrospector.INSTANCE,
BeanArbitraryIntrospector.INSTANCE
)
))
.build();
Best Practices
When creating custom introspectors:
- Always check the type before processing, returning
NOT_INTROSPECTED
for types your introspector doesn’t handle - Handle exceptions gracefully to prevent test failures
- Keep it focused - each introspector should handle a specific pattern or class type
- Consider performance since introspectors run for every object creation
- Test thoroughly with various edge cases
Advanced: Property Generators
Fixture Monkey provides several built-in PropertyGenerator
implementations that can help with custom property discovery:
FieldPropertyGenerator
Useful for creating properties based on class fields:
// Generate properties based on fields with specific conditions
new FieldPropertyGenerator(
// Only non-final fields with a specific annotation
field -> !Modifier.isFinal(field.getModifiers()) &&
field.isAnnotationPresent(MyRequired.class),
// Include all fields that pass the filter
field -> true
)
CompositePropertyGenerator
Combines multiple property generators:
// Use both field and JavaBeans property generation together
new CompositePropertyGenerator(
Arrays.asList(
new FieldPropertyGenerator(field -> true, matcher -> true),
new JavaBeansPropertyGenerator(
descriptor -> descriptor.getReadMethod() != null,
matcher -> true
)
)
)
DefaultPropertyGenerator
A pre-configured combination of common generators:
// Uses standard field and JavaBeans property generation
new DefaultPropertyGenerator()
Conclusion
Creating custom introspectors is an advanced topic, but it gives you complete control over object creation in Fixture Monkey. Most users won’t need this level of customization, but it’s available when you have special requirements that the built-in introspectors can’t handle.
If you have any questions about custom introspectors, refer to the source code of the built-in introspectors for examples of different implementation approaches.