Concepts
This document explains the core concepts and terminology of Fixture Monkey’s option system. Understanding these concepts will help you navigate and use Fixture Monkey more effectively.
For practical implementation of these concepts, see:
- Essential Options for Beginners - Start here if you’re new to Fixture Monkey
- Advanced Options for Experts - Explore more complex options
Key Terms to Know
Before diving into detailed concepts, let’s understand the fundamental terminology used throughout Fixture Monkey:
Term | Description |
---|---|
Introspection | How Fixture Monkey analyzes the structure and properties of objects |
Arbitrary | A generator that produces random values for a specific type |
Container | A structure that holds multiple values, like collections, maps, or arrays |
Property | A characteristic of an object that can hold a value - can be implemented as a field, getter/setter method, or Kotlin property. Properties contain information about their type, annotations, and name. |
Constraint | A rule that limits the range of values that can be generated (e.g., min/max values) |
Fixture Monkey provides numerous options that can be configured through the FixtureMonkeyBuilder
.
This page explains core option concepts to help you understand how they work together.
Core Components of Option Architecture
Understanding these core components will help you navigate the options more effectively:
1. Generators and Introspectors
These are the primary components that work together to create objects:
Generator:
- Has a broader scope for creating values with specific characteristics
- Focuses on the “what” of value generation (unique values, patterns, ranges, etc.)
- Examples:
UniqueArbitraryGenerator
- In simple terms: Generators are like cookie cutters that determine what shape your test data will take
Introspector:
- Specifies the concrete method of object creation
- Focuses on the “how” of object instantiation (via constructors, factories, etc.)
- Examples:
ConstructorArbitraryIntrospector
,FieldReflectionArbitraryIntrospector
- In simple terms: Introspectors are like bakers who figure out how to actually make your object using the available ingredients
For a beginner, you can think of generators as defining “what kind of data” to create, while introspectors determine “how to actually construct” the object.
Practical example: When you need to test a payment system with valid credit card numbers, generators help you create properly formatted numbers, while introspectors determine whether to create them through a constructor, builder pattern, or factory method.
For implementation details on generators, see:
- Type Configuration in Essential Options
- Custom Type Registration and Generation in Advanced Options
2. Builder Pattern and Option Chaining
Fixture Monkey uses a builder pattern for configuration. Understanding option application order is important:
FixtureMonkey monkey = FixtureMonkey.builder()
.plugin(new KotlinPlugin()) // Applied first
.nullableContainer(true) // Applied second
.defaultArbitraryContainerSize(3, 5) // Applied third
.build();
Options are applied in the order they’re defined. Later options can override earlier ones if they target the same settings.
Practical example: If you need to test how your application handles different product lists, you might set a default container size, but override it for specific test cases to test boundary conditions.
3. Option Scoping
Options can be applied at different scopes or levels:
Global Scope:
- Applied to all objects generated by the Fixture Monkey instance
- Set in
FixtureMonkeyBuilder
- Example use case: Setting
defaultNotNull(true)
to avoid null values throughout your tests
Type Scope:
- Applied to specific types or interfaces
- Example:
.pushAssignableTypePropertyGenerator(String.class, generator)
- Example use case: Configuring all String values to follow a specific pattern like email addresses
Path Expression Scope:
- Applied to specific properties identified by path expressions
- Example:
.register(String.class, fixtureMonkey -> fixtureMonkey.giveMeBuilder(String.class).set("$", Arbitraries.strings().ofMinLength(3)))
- Example use case: Setting a specific property like “price” to be always positive
The more specific scope overrides the more general one - similar to how CSS specificity works if you’re familiar with web development.
For practical usage of option scoping, see:
- General Builder Options in Essential Options
- Advanced Customization in Advanced Options
4. Type Registration System
Fixture Monkey’s type registration system determines how to generate values for specific types:
FixtureMonkey monkey = FixtureMonkey.builder()
.register(String.class, fixtureMonkey -> fixtureMonkey.giveMeBuilder(String.class)
.set("$", Arbitraries.strings().ofMinLength(5).ofMaxLength(10)))
.register(Integer.class, fixtureMonkey -> fixtureMonkey.giveMeBuilder(Integer.class)
.set("$", Arbitraries.integers().between(1, 100)))
.build();
This registers custom generators for specific types, which can then be used in all objects that contain these types.
Practical example: If your application requires all user IDs to be between 1 and 100, you can register this rule once and all test objects will follow it.
For details on implementing type registration, see:
- Type Configuration in Essential Options
- Custom Type Registration and Generation in Advanced Options
5. Plugin System
Plugins provide pre-configured settings for common use cases:
FixtureMonkey monkey = FixtureMonkey.builder()
.plugin(new KotlinPlugin())
.plugin(new JacksonPlugin())
.build();
Plugins can:
- Register type-specific generators and introspectors
- Configure default behaviors
- Add support for third-party libraries
In simple terms: Plugins are like recipe books with pre-configured settings for common scenarios.
Practical example: If you’re working with a Kotlin project, the KotlinPlugin automatically configures everything to work well with Kotlin classes, saving you from writing lots of manual configuration.
For plugin usage examples, see:
- JqwikPlugin Options in Essential Options
- Advanced Plugin Customization in Advanced Options
Properties and Containers
Fixture Monkey uses a broader concept of property
instead of just field
when working with objects. This flexibility allows Fixture Monkey to work with various programming paradigms and frameworks.
Properties in Fixture Monkey
A property
in Fixture Monkey contains:
- Its
Type
(String, Integer, custom class, etc.) - Any
Annotation
s applied to it - Its
name
Fixture Monkey distinguishes between two main property types:
1. ObjectProperty
Represents information about a regular object property, including:
- The property itself
- How the property name is resolved
- If it’s part of a collection, its index
Example use in code:
// ObjectProperty in action - setting a value on a specific property
Product product = fixtureMonkey.giveMeBuilder(Product.class)
.set("name", "Smartphone") // ObjectProperty: name
.set("price", BigDecimal.valueOf(599.99)) // ObjectProperty: price
.sample();
2. ContainerProperty
Represents collections with information about:
- The list of element properties
- Information about the container’s size
Example use in code:
// ContainerProperty in action - working with a collection
List<Product> products = fixtureMonkey.giveMeBuilder(new TypeReference<List<Product>>() {})
.size(3) // Set container size
.set("$[0].name", "First Product") // Access first element's property
.sample();
Container Types vs. Object Types
Fixture Monkey treats container types (like List
, Set
, or Map
) differently from regular object types. This distinction is important for testing scenarios involving collections.
Why this matters for your tests: Understanding this distinction helps you correctly configure collection sizes, element properties, and uniqueness constraints.
Real-world example:
// Testing a shopping cart with multiple items
ShoppingCart cart = fixtureMonkey.giveMeBuilder(ShoppingCart.class)
.size("items", 3) // ContainerProperty: 3 items in the cart
.set("items[0].productName", "Special Item") // First item has a specific name
.set("customer.address.country", "USA") // ObjectProperty with nested path
.sample();
For deeper details on property handling, see:
- Custom Object Introspection in Essential Options
- Object Property Generators in Advanced Options
- Container Handling Options in Advanced Options
How Options Interact
When Fixture Monkey generates an object:
- It first determines which introspector to use for the object
- The introspector analyzes the object structure
- For each property, it selects the appropriate generator
- Property generators create values according to configuration
- The introspector assembles the final object
Options at more specific scopes take precedence over more general ones.
Practical example: If you set a global rule that all numbers should be positive, but then specify that a particular “discount” field should be negative, the specific rule for the discount field will override the global rule.
Visual Explanation
Here’s a simplified view of how these components interact:
┌─────────────────────────────────────────┐
│ FixtureMonkeyBuilder │
└───────────────┬─────────────────────────┘
│
▼
┌───────────────────────────────────┐ ┌───────────────────┐
│ Option Resolution │◄───┤ Plugins │
└───────────────┬───────────────────┘ └───────────────────┘
│
▼
┌───────────────────────────────────┐
│ Type/Property Selection │
└───────────────┬───────────────────┘
│
▼
┌──────────────┐ ┌──────────────┐
│ Introspector │◄─────┤ Generators │
└──────────┬───┘ └──────────────┘
│
▼
┌───────────────────────────────────┐
│ Generated Object │
└───────────────────────────────────┘
Example: Option Interaction
Let’s see a more concrete example where multiple options interact:
FixtureMonkey monkey = FixtureMonkey.builder()
.plugin(new JavaTimePlugin()) // Global plugin
.defaultArbitraryContainerSize(1, 5) // Global container size
.nullableContainer(true) // Global container nullability
.register(String.class, fixtureMonkey -> fixtureMonkey.giveMeBuilder(String.class)
.set("$", Arbitraries.strings().ofMinLength(3))) // Type-specific configuration
.register(
new MatcherOperator<>(
property -> property.getName().equals("price"), // Match any property named "price"
fixtureMonkey -> fixtureMonkey.giveMeBuilder(BigDecimal.class)
.set("$", Arbitraries.bigDecimals().greaterOrEqual(BigDecimal.ZERO))
)
) // Simple property name matcher
.build();
When generating an object:
- The JavaTimePlugin configures date/time types
- Containers are set to have 1-5 elements and can be null
- All String values have minimum length 3
- Any property named “price” is always non-negative
Real-world scenario: This configuration would be useful for testing an e-commerce application where:
- You need to test with various product counts (1-5 items)
- Product descriptions need to be at least 3 characters long
- Prices must always be positive or zero
For examples of complex configuration, see Real-World Advanced Configuration in Advanced Options.
Conclusion
Understanding these concepts will help you:
- Configure Fixture Monkey more effectively for your specific testing needs
- Troubleshoot issues when generators don’t work as expected
- Create complex test data with precise control
By leveraging properties, containers, and the option system effectively, you can create more realistic and targeted test data that closely matches your production requirements.
Next Steps
After understanding these concepts, you can:
→ Essential Options for Beginners - Learn practical implementation of these concepts
→ Advanced Options for Experts - Explore advanced options for complex testing scenarios
→ Creating Custom Introspector - Implement your own introspector for special domain requirements