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:

Key Terms to Know

Before diving into detailed concepts, let’s understand the fundamental terminology used throughout Fixture Monkey:

TermDescription
IntrospectionHow Fixture Monkey analyzes the structure and properties of objects
ArbitraryA generator that produces random values for a specific type
ContainerA structure that holds multiple values, like collections, maps, or arrays
PropertyA 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.
ConstraintA 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:

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:

  1. 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
  2. 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
  3. 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:

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:

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:

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 Annotations 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:

How Options Interact

When Fixture Monkey generates an object:

  1. It first determines which introspector to use for the object
  2. The introspector analyzes the object structure
  3. For each property, it selects the appropriate generator
  4. Property generators create values according to configuration
  5. 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:

  1. The JavaTimePlugin configures date/time types
  2. Containers are set to have 1-5 elements and can be null
  3. All String values have minimum length 3
  4. 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