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
| Scenario | Recommended |
|---|---|
| Simple property access | Path expressions (set("field", value)) |
| Map properties | InnerSpec (not possible with path expressions) |
| Reusable customization patterns | InnerSpec |
| Complex nested structures | InnerSpec |
| Concise one-off customization | Path expressions |
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:
- Java
- Kotlin
InnerSpec innerSpec = new InnerSpec().property("id", 1000);
fixtureMonkey.giveMeBuilder(Product.class)
.setInner(innerSpec);
val innerSpec = InnerSpec().property("id", 1000)
fixtureMonkey.giveMeBuilder<Product>()
.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.
Fixture Monkey expressions such as refering elements ([]) or nested fields(.) are not allowed as the property name. Only the property name itself is allowed.
- Java
- Kotlin
InnerSpec innerSpec = new InnerSpec()
.property("id", 1000);
val innerSpec = 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.
- Java
- Kotlin
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
val innerSpec = InnerSpec()
.property("options") { it.size(5) } // size:5
val innerSpec = InnerSpec()
.property("options") { it.size(3, 5) } // minSize:3, maxSize:5
val innerSpec = InnerSpec()
.property("options") { it.minSize(3) } // minSize:3
val innerSpec = InnerSpec()
.property("options") { it.maxSize(5) } // maxSize:5
postCondition()
postCondition() can be used when you require your property to match a specific condition.
Using setPostCondition can incur higher costs for narrow conditions. In such cases, it's recommended to use set instead.
- Java
- Kotlin
InnerSpec innerSpec = new InnerSpec()
.property("id", id -> id.postCondition(Long.class, it -> it > 0));
val innerSpec = InnerSpec()
.property("id") { it.postCondition(Long::class.java) { 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)
- Java
- Kotlin
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
);
val addressSpec = InnerSpec()
.property("street", "123 Main St")
.property("zipCode", "12345")
val personSpec = InnerSpec()
.property("name", "John Doe")
.property("address") { address ->
address.inner(addressSpec)
.property("additionalField", "extra")
}
Customizing list properties
listElement()
Individual elements within lists can be selected using listElement().
This is equivalent to referencing elements with "[n]" using path expressions.
- Java
- Kotlin
InnerSpec innerSpec = new InnerSpec()
.property("options", options -> options.listElement(0, "red"));
val innerSpec = InnerSpec()
.property("options") { it.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.
- Java
- Kotlin
InnerSpec innerSpec = new InnerSpec()
.property("options", options -> options.allListElement("red"));
val innerSpec = InnerSpec()
.property("options") { it.allListElement("red") }
Customizing map properties
InnerSpec provides special methods for customizing map property entries.
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 randomizedvalue(): assigns a specified value to the value, while the key remains randomizedentry(): specifies both the key and value at once
- Java
- Kotlin
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"));
val innerSpec = InnerSpec()
.property("merchantInfo") { it.key(1000) }
val innerSpec = InnerSpec()
.property("merchantInfo") { it.value("ABC Store") }
val innerSpec = InnerSpec()
.property("merchantInfo") { it.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.
- Java
- Kotlin
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"));
val innerSpec = InnerSpec()
.property("merchantInfo") { it.keys(1000, 1001, 1002) }
val innerSpec = InnerSpec()
.property("merchantInfo") { it.values("ABC Store", "123 Convenience", "XYZ Mart") }
val innerSpec = InnerSpec()
.property("merchantInfo") { it.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().
- Java
- Kotlin
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"));
val innerSpec = InnerSpec()
.property("merchantInfo") { it.allKey(1000) }
val innerSpec = InnerSpec()
.property("merchantInfo") { it.allValue("ABC Store") }
val innerSpec = InnerSpec()
.property("merchantInfo") { it.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.
- Java
- Kotlin
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));
val innerSpec = InnerSpec()
.property("merchantInfo") { it.keyLazy(this::generateMerchantKey) }
val innerSpec = InnerSpec()
.property("merchantInfo") { it.valueLazy(this::generateMerchantValue) }
val innerSpec = InnerSpec()
.property("merchantInfo") { it.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.
- Java
- Kotlin
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));
val innerSpec = InnerSpec()
.property("merchantInfo") { it.allKeyLazy(this::generateMerchantKey) }
val innerSpec = InnerSpec()
.property("merchantInfo") { it.allValueLazy(this::generateMerchantValue) }
val innerSpec = InnerSpec()
.property("merchantInfo") { it.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
- Java
- Kotlin
// 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"));
// 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
- Java
- Kotlin
// 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")));
// 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"));