Skip to main content
Version: v1.1.x

InnerSpec

What you will learn in this document

  • How to customize complex object structures more granularly
  • How to effectively handle Map type properties
  • How to create reusable customization specifications

Introduction to InnerSpec

InnerSpec is a powerful tool in Fixture Monkey that helps you customize complex nested objects in a structured way. Think of it as a reusable "specification" of how you want your objects to be customized.

You might want to use InnerSpec when:

  • You need to customize deeply nested objects
  • You're working with Map type properties (which can't be easily customized with regular path expressions)
  • You want to create reusable customization patterns across multiple tests

An InnerSpec is a type-independent specification for the customizations you wish to apply. Using the setInner() method within ArbitraryBuilder, you can apply customizations defined within an InnerSpec instance into your builder.

A Simple Example

// Create an InnerSpec to customize product properties
InnerSpec productSpec = new InnerSpec()
.property("id", 1000L)
.property("name", "Smartphone")
.property("price", new BigDecimal("499.99"));

// Apply the InnerSpec to a Product builder
Product product = fixtureMonkey.giveMeBuilder(Product.class)
.setInner(productSpec)
.sample();

// Now product has id=1000, name="Smartphone", price=499.99

When to use InnerSpec vs Path Expressions

ScenarioRecommended
Simple property accessPath expressions (set("field", value))
Map propertiesInnerSpec (not possible with path expressions)
Reusable customization patternsInnerSpec
Complex nested structuresInnerSpec
Concise one-off customizationPath expressions
tip

Kotlin EXP is not supported for InnerSpec, as it is designed to be type-independent. Instead, you need to specify the property by its name.

Applying InnerSpec to the ArbitraryBuilder

To apply your pre-defined InnerSpec to the builder, use the setInner() method:

InnerSpec innerSpec = new InnerSpec().property("id", 1000);

fixtureMonkey.giveMeBuilder(Product.class)
.setInner(innerSpec);

Customizing properties

property()

Similar to the set() method in ArbitraryBuilder, you can customize a property by specifying its name and providing the desired value.

danger

Fixture Monkey expressions such as refering elements ([]) or nested fields(.) are not allowed as the property name. Only the property name itself is allowed.

InnerSpec innerSpec = new InnerSpec()
.property("id", 1000);

size(), minSize(), maxSize()

size(), minSize(), and maxSize() can be used to specify the size of the property.

You can first select the container property using property() and then proceed to define an innerSpec consumer to set the size.

InnerSpec innerSpec = new InnerSpec()
.property("options", options -> options.size(5)); // size:5

InnerSpec innerSpec = new InnerSpec()
.property("options", options -> options.size(3, 5)); // minSize:3, maxSize:5

InnerSpec innerSpec = new InnerSpec()
.property("options", options -> options.minSize(3)); // minSize:3

InnerSpec innerSpec = new InnerSpec()
.property("options", options -> options.maxSize(5)); // maxSize:5

postCondition()

postCondition() can be used when you require your property to match a specific condition.

danger

Using setPostCondition can incur higher costs for narrow conditions. In such cases, it's recommended to use set instead.

InnerSpec innerSpec = new InnerSpec()
.property("id", id -> id.postCondition(Long.class, it -> it > 0));

inner()

You can customize a property using another pre-defined InnerSpec with the help of inner().

There are two approaches for handling nested objects:

Approach 1: Directly passing an InnerSpec object (simpler)

InnerSpec addressSpec = new InnerSpec()
.property("street", "123 Main St")
.property("zipCode", "12345");

InnerSpec personSpec = new InnerSpec()
.property("name", "John Doe")
.property("address", addressSpec); // Directly passing the InnerSpec

Approach 2: Using the inner() method (more flexible, allows additional customization)

InnerSpec addressSpec = new InnerSpec()
.property("street", "123 Main St")
.property("zipCode", "12345");

InnerSpec personSpec = new InnerSpec()
.property("name", "John Doe")
.property("address", address -> address
.inner(addressSpec)
.property("additionalField", "extra") // Can add more customization
);

Customizing list properties

listElement()

Individual elements within lists can be selected using listElement(). This is equivalent to referencing elements with "[n]" using path expressions.

InnerSpec innerSpec = new InnerSpec()
.property("options", options -> options.listElement(0, "red"));

allListElement()

If you wish to set all elements of the list simultaneously, you can use allListElement(). This is equivalent to referencing elements with "[*]" using path expressions.

InnerSpec innerSpec = new InnerSpec()
.property("options", options -> options.allListElement("red"));

Customizing map properties

InnerSpec provides special methods for customizing map property entries.

danger

Similar to lists, setting a map entry without specifying the size first might lead to no change. Prior to setting a value, ensure that the map property has the intended size.

key(), value(), entry()

You can customize map property entries using key(), value(), and entry() methods.

  • key(): assigns a specified value to the key, while the entry's value remains randomized
  • value(): assigns a specified value to the value, while the key remains randomized
  • entry(): specifies both the key and value at once
InnerSpec innerSpec = new InnerSpec()
.property("merchantInfo", merchantInfo -> merchantInfo.key(1000));

InnerSpec innerSpec = new InnerSpec()
.property("merchantInfo", merchantInfo -> merchantInfo.value("ABC Store"));

InnerSpec innerSpec = new InnerSpec()
.property("merchantInfo", merchantInfo -> merchantInfo.entry(1000, "ABC Store"));

keys(), values(), entries()

When setting multiple entries within a map, you can use keys(), values(), and entries() to pass multiple values.

InnerSpec innerSpec = new InnerSpec()
.property("merchantInfo", merchantInfo -> merchantInfo.keys(1000, 1001, 1002));

InnerSpec innerSpec = new InnerSpec()
.property("merchantInfo", merchantInfo -> merchantInfo.values("ABC Store", "123 Convenience", "XYZ Mart"));

InnerSpec innerSpec = new InnerSpec()
.property("merchantInfo", merchantInfo -> merchantInfo.entries(1000, "ABC Store", 1001, "123 Convenience", 1002, "XYZ Mart"));

allKey(), allValue(), allEntry()

Similar to allListElement(), it is possible to set every entry within the map to the specified value with allKey(), allValue(), and allEntry().

InnerSpec innerSpec = new InnerSpec()
.property("merchantInfo", merchantInfo -> merchantInfo.allKey(1000));

InnerSpec innerSpec = new InnerSpec()
.property("merchantInfo", merchantInfo -> merchantInfo.allValue("ABC Store"));

InnerSpec innerSpec = new InnerSpec()
.property("merchantInfo", merchantInfo -> merchantInfo.allEntry(1000, "ABC Store"));

keyLazy(), valueLazy(), entryLazy()

Similar to the setLazy() method in ArbitraryBuilder, you can pass a Supplier to assign the value. The Supplier will run every time the ArbitraryBuilder with the InnerSpec applied is sampled.

InnerSpec innerSpec = new InnerSpec()
.property("merchantInfo", merchantInfo -> merchantInfo.keyLazy(this::generateMerchantKey));

InnerSpec innerSpec = new InnerSpec()
.property("merchantInfo", merchantInfo -> merchantInfo.valueLazy(this::generateMerchantValue));

InnerSpec innerSpec = new InnerSpec()
.property("merchantInfo", merchantInfo -> merchantInfo.entryLazy(this::generateMerchantKey, this::generateMerchantValue));

allKeyLazy(), allValueLazy(), allEntryLazy()

Just as with the allKey() method, you can use allKeyLazy() to apply keyLazy() to every entry within the map. Both allValueLazy() and allEntryLazy() function similarly.

InnerSpec innerSpec = new InnerSpec()
.property("merchantInfo", merchantInfo -> merchantInfo.allKeyLazy(this::generateMerchantKey));

InnerSpec innerSpec = new InnerSpec()
.property("merchantInfo", merchantInfo -> merchantInfo.allValueLazy(this::generateMerchantValue));

InnerSpec innerSpec = new InnerSpec()
.property("merchantInfo", merchantInfo -> merchantInfo.allEntryLazy(this::generateMerchantKey, this::generateMerchantValue));

Customizing nested Maps

By combining methods within InnerSpec, you can effectively customize maps with map-type keys, map-type values, or both.

public class Example {
Map<Map<String, String>, String> mapByString;
Map<String, Map<String, String>> stringByMap;
}

Setting map-type key

// Set a map key
InnerSpec().property("mapByString", m -> m.key(k -> k.entry("key", "value")));

// Set a full entry with map-type key
InnerSpec().property("mapByString", m -> m.entry(k -> k.entry("innerKey", "innerValue"), "value"));

Setting map-type value

// Set a map value
InnerSpec().property("stringByMap", m -> m.value(v -> v.entry("key", "value")));

// Set a full entry with map-type value
InnerSpec().property("stringByMap", m -> m.entry("key", v -> v.entry("innerKey", "innerValue")));

Common Mistakes and Solutions

1. Not Setting Collection Size First

// Wrong: entry might not be added
InnerSpec innerSpec = new InnerSpec()
.property("merchantInfo", merchantInfo -> merchantInfo.entry(1000, "ABC Store"));

// Correct: set size first
InnerSpec innerSpec = new InnerSpec()
.property("merchantInfo", merchantInfo -> merchantInfo
.size(1)
.entry(1000, "ABC Store")
);

2. Map Key/Value Type Mismatch

// Wrong: key should be Long but using String
new InnerSpec().property("merchantInfo", m -> m.entry("key", "ABC Store"));

// Correct: match the map's key type
new InnerSpec().property("merchantInfo", m -> m.entry(1000L, "ABC Store"));