Fixture Monkey

repository·main·Indexed 20 days ago

https://github.com/naver/fixture-monkey

A Java and Kotlin library for generating controllable, arbitrary test objects. It simplifies JVM testing by providing a way to create complex test fixtures with minimal boilerplate using path-based configuration, ArbitraryBuilder for customization, and various ArbitraryIntrospectors to support different class structures like Records, Data classes, and JavaBeans.

Tokens
66.2K
Snippets
189
Records
266
Agent score
71%

What's inside Fixture Monkey

  1. What is InnerSpec and when to use it

    main

    InnerSpec is a type-independent specification used to customize complex, nested object structures in a structured and reusable way. Unlike standard path expressions, InnerSpec allows for granular control over deeply nested objects and is the required method for customizing Map type properties, which cannot be easily targeted with regular path expressions.

    Comparison: InnerSpec vs Path Expressions

    ScenarioRecommended Method
    Simple property accessPath expressions (set("field", value))
    Map propertiesInnerSpec
    Reusable customization patternsInnerSpec
    Complex nested structuresInnerSpec
    Concise one-off customizationPath expressions
    TIP

    In Kotlin, because InnerSpec is designed to be type-independent, Kotlin Expression Language (EXP) is not supported. You must specify properties by their exact name.

  2. How components in Fixture Monkey interact

    main

    Fixture Monkey follows a structured pipeline to transform configuration into a generated object. The process flows as follows:

    1. FixtureMonkeyBuilder: The entry point where you define plugins, global settings, and type-specific registrations.
    2. Option Resolution: The builder resolves the configured options and integrates any registered Plugins.
    3. Type/Property Selection: The system determines which types and specific properties need to be addressed based on the target object.
    4. Introspector & Generators: The Introspector analyzes the structure of the target type, while Generators provide the actual values. These two components work together to produce the final data.
    5. Generated Object: The final result of the pipeline.
  3. Options vs ArbitraryBuilder API

    main

    Fixture Monkey provides two distinct ways to configure test data generation, depending on the required scope:

    1. Options: Configured during FixtureMonkey instance creation via the builder. These define global rules that apply to all generated data and are highly reusable across multiple tests.
    2. ArbitraryBuilder API: Configured during individual test data creation (e.g., using .giveMeBuilder(...)). These are one-time settings used for fine-grained control over a specific test case.

    Use Options for consistency and maintainability (e.g., ensuring all Product objects have a positive price) and the ArbitraryBuilder API for overrides specific to a single test (e.g., setting a specific name for one test case).

    // Using options - applies to all Product instances globally
    FixtureMonkey fixtureMonkey = FixtureMonkey.builder()
        .defaultNotNull(true)
        .register(Product.class, fm -> fm.giveMeBuilder(Product.class)
            .size("items", 3))
        .build();
    
    // Using ArbitraryBuilder API - applies only to this specific test instance
    Product specificProduct = fixtureMonkey.giveMeBuilder(Product.class)
        .set("name", "Test Product")
        .set("price", 1000)
        .sample();
  4. How to choose an interface customization approach

    main

    Choose your approach based on your testing requirements:

    ScenarioRecommended Approach
    Quick test, don't care about implementation detailsAnonymous implementation
    Already have an instance, no further customization neededValues.just
    Need to customize implementation-specific propertiesinterfaceImplements
    Need Fixture Monkey to randomly select among implementationsinterfaceImplements
    Reusing the same implementations across multiple testsinterfaceImplements
  5. Reuse test specifications with ArbitraryBuilder

    main

    You can define a base configuration using giveMeBuilder and reuse it across multiple tests. This helps eliminate duplication and ensures consistency in your test data.

    // Define once, reuse everywhere
    ArbitraryBuilder<Product> productBuilder = fixtureMonkey.giveMeBuilder(Product.class)
        .set("category", "Book")
        .set("price", 1000);
    
    // Reuse in different tests
    Product product1 = productBuilder.sample();  
    Product product2 = productBuilder.size("reviews", 3).sample();
  6. When to use the Jackson Plugin

    main

    The Jackson plugin is recommended in the following scenarios:

    • Your classes use Jackson annotations like @JsonProperty, @JsonIgnore, @JsonTypeInfo, or @JsonSubTypes.
    • Your project contains a mix of Java and Kotlin classes.
    • Your classes lack a no-args constructor or Lombok annotations.
    • You want object creation to strictly follow your production serialization logic.

    Alternatives:

    • If your classes have a no-args constructor with setters, the default BeanArbitraryIntrospector is sufficient.
    • For Lombok @Value classes, use ConstructorPropertiesArbitraryIntrospector instead.
  7. Compare set() and customizeProperty()

    main

    Choosing between set() and customizeProperty() depends on whether you want to provide a specific value or influence the generation process.

    Featureset()customizeProperty()
    Primary UseAssigns a specific, fixed value.Modifies how values are generated.
    LogicDirect assignment.Supports transformations (map) and filtering (filter).
    ComplexityLow.Higher (requires selectors and Arbitrary manipulation).

    Example Comparison:

    Direct assignment with set():

    Member member = fixtureMonkey.giveMeBuilder(Member.class)
        .set("email", "john@test.com")
        .sample();

    Transformation with customizeProperty():

    Member memberWithCustomEmail = fixtureMonkey.giveMeBuilder(Member.class)
        .customizeProperty("email", arb -> arb.map(email -> "vip-" + email))
        .sample();
  8. Use JacksonObjectArbitraryIntrospector via JacksonPlugin

    main

    The JacksonObjectArbitraryIntrospector is automatically set as the default introspector when the JacksonPlugin is added to the FixtureMonkey builder.

    This introspector works by collecting the properties of a class into a map and then using a Jackson ObjectMapper to deserialize them into an object. This makes it a highly compatible, general-purpose choice, especially in mixed Java and Kotlin environments, because it leverages Jackson's existing logic for object creation.

    Note on Performance: While highly compatible, it may be less efficient than other introspectors because it relies on the full Jackson deserialization process.

    @Test
    void test() {
        FixtureMonkey fixtureMonkey = FixtureMonkey.builder()
            .plugin(new JacksonPlugin())
            .build();
    
        Product product = fixtureMonkey.giveMeOne(Product.class);
    }
  9. Use the Interface Plugin to handle interfaces and abstract classes

    main

    The InterfacePlugin allows Fixture Monkey to dynamically resolve concrete implementations for interfaces and abstract classes during object generation. This is essential when your test fixtures involve types that cannot be instantiated directly.

    Core Capabilities

    • Register specific concrete implementations for interfaces.
    • Register specific concrete implementations for abstract classes.
    • Use an AnonymousArbitraryIntrospector (enabled by default) to create dynamic proxy implementations for interfaces that don't have registered concrete classes.
    • Use a CandidateConcretePropertyResolver for dynamic, runtime-based implementation resolution based on property characteristics.
    FixtureMonkey sut = FixtureMonkey.builder()
        .plugin(new InterfacePlugin()
            .interfaceImplements(MyInterface.class, Arrays.asList(MyInterfaceImpl1.class, MyInterfaceImpl2.class))
            .abstractClassExtends(MyAbstractClass.class, Arrays.asList(MyConcreteClass1.class, MyConcreteClass2.class))
        )
        .build();
  10. Understand the Fixture Monkey Resolution Trace

    main

    When using AssemblyTracer.console(), the output is organized into several sections to help you trace the lifecycle of a generated object.

    Key sections include:

    • Builder Context: Shows if isFixed (deterministic mode) or validOnly (strict mode) are active.
    • Analysis: Shows applied directives (e.g., set, setLazy, container size) with their application order (seq) and source (DIRECT, REGISTER, etc.).
    • Values by Path: The raw mapping of paths to values analyzed during the adapt phase.
    • Node Collisions: Identifies paths where a later set() call overwrote an earlier value.
    • Manipulator Overrides: Shows which directives won when multiple target the same path.
    • Merged Candidates: The final set of values (User-set + Registered) fed into the assembly phase.
    • Interface Resolutions: Explains how an interface or abstract type was resolved to a concrete type.
    • Container Size Resolutions: Explains how container sizes (lists, sets, etc.) were decided (e.g., EXACT_PATH, TYPE_BASED).
    • Unresolved Paths: In strict mode, shows paths that failed to match, including the reason and available fields.
    • Assembly: Details how each node was created, including the introspector and creation method used.
    • Timing Information: Per-phase timings (Analyze, Tree Build, Assembly, etc.).
  11. Understanding Arbitrary in Fixture Monkey

    main

    In Fixture Monkey, Arbitrary is a core abstraction used to create random test data that follows specific rules. Instead of providing a single fixed value, you use an Arbitrary to define a value generator with rules. This allows you to generate a wide range of valid, realistic, and controlled inputs (like specific numeric ranges, string patterns, or sets of valid options) for your tests.

    Use Arbitrary when:

    • You want to test a variety of inputs rather than a single value.
    • The exact value is less important than the fact that it follows business rules.
    • You want to discover edge cases automatically.
    • You need to generate many different valid inputs efficiently.