Skip to main content
Version: v1.1.x

Best Practices

This guide provides practical tips and best practices for using Fixture Monkey effectively in your tests.

1. Keep Tests Simple and Focused

  • Only customize what matters for the test: Don't set values for fields that don't affect the test's behavior.
// Good - only set what's relevant to the test
@Test
void shouldCalculateDiscount() {
Product product = fixtureMonkey.giveMeBuilder(Product.class)
.set("price", 100.0) // Only price matters for discount calculation
.sample();

double discount = productService.calculateDiscount(product);

assertThat(discount).isEqualTo(10.0);
}

2. Prefer Direct Property Setting Over Post-Conditions

  • Use direct property setting when possible: Instead of using setPostCondition which can cause performance issues due to rejection sampling, prefer direct configuration with set or size.
// Less efficient - uses post-conditions with rejection sampling
@Test
void lessEfficientOrderTest() {
Order order = fixtureMonkey.giveMeBuilder(Order.class)
.setPostCondition(o -> o.getItems().size() > 0) // Performance cost: may reject many samples
.setPostCondition(o -> o.getTotalAmount() > 100) // Additional performance cost
.sample();

OrderResult result = orderService.process(order);

assertThat(result.isSuccessful()).isTrue();
}

// More efficient - uses direct property setting
@Test
void moreEfficientOrderTest() {
Order order = fixtureMonkey.giveMeBuilder(Order.class)
.size("items", 1, 5) // Directly set collection size
.set("totalAmount", Arbitraries.integers().greaterThan(100)) // Directly set valid range
.sample();

OrderResult result = orderService.process(order);

assertThat(result.isSuccessful()).isTrue();
}

// When to use setPostCondition - for truly complex validation that cannot be expressed with property setting
@Test
void complexValidationTest() {
// Only use setPostCondition for complex validations that cannot be expressed otherwise
Invoice invoice = fixtureMonkey.giveMeBuilder(Invoice.class)
.set("items", fixtureMonkey.giveMe(InvoiceItem.class, 3))
.set("customerType", CustomerType.BUSINESS)
.setPostCondition(inv -> inv.calculateTotal().compareTo(inv.getItems().stream()
.map(InvoiceItem::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add)) == 0)
.sample();

assertThat(invoiceService.validate(invoice)).isTrue();
}

3. Avoid Over-Specification in Tests

  • Don't overspecify test requirements: Test only what needs to be tested.
// Bad - overspecified test with unnecessary details
@Test
void badTestTooManyDetails() {
User user = fixtureMonkey.giveMeBuilder(User.class)
.set("id", 1L)
.set("name", "John")
.set("email", "john@example.com")
.set("address.street", "123 Main St")
.set("address.city", "New York")
.set("address.zipCode", "10001")
.set("registrationDate", LocalDate.of(2023, 1, 1))
.sample();

// Test is just checking if email is valid
assertThat(userValidator.isEmailValid(user)).isTrue();
}

// Good - only specify what's needed for the test
@Test
void goodTestOnlyNeededDetails() {
User user = fixtureMonkey.giveMeBuilder(User.class)
.set("email", "john@example.com") // Only email matters for this test
.sample();

assertThat(userValidator.isEmailValid(user)).isTrue();
}

4. Make Tests Readable with Helper Methods

  • Create helper methods to improve test readability: Encapsulate fixture setup for better readability.
@Test
void testOrderProcessing() {
// Helper methods returning ArbitraryBuilder for more flexibility
Order standardOrder = standardOrderBuilder()
.set("customerNote", "Please deliver quickly") // Test-specific customization
.sample();

Customer premiumCustomer = premiumCustomerBuilder()
.set("membershipYears", 5) // Test-specific customization
.sample();

OrderResult result = orderService.process(standardOrder, premiumCustomer);

assertThat(result.hasDiscount()).isTrue();
assertThat(result.getDiscount()).isGreaterThanOrEqualTo(standardOrder.getTotalAmount() * 0.1);
}

// Helper methods return ArbitraryBuilder instead of instances
private ArbitraryBuilder<Order> standardOrderBuilder() {
return fixtureMonkey.giveMeBuilder(Order.class)
.size("items", 3, 5)
.set("totalAmount", Arbitraries.integers().between(100, 500));
}

private ArbitraryBuilder<Customer> premiumCustomerBuilder() {
return fixtureMonkey.giveMeBuilder(Customer.class)
.set("premiumMember", true)
.set("membershipYears", 2);
}

@Test
void testOrderWithSpecialDiscount() {
// Reuse the same builder with different customizations
Order bulkOrder = standardOrderBuilder()
.size("items", 10, 20) // Different configuration for this test
.set("totalAmount", Arbitraries.integers().between(500, 1000))
.sample();

Customer vipCustomer = premiumCustomerBuilder()
.set("membershipYears", 10) // Different configuration for this test
.set("vipStatus", true)
.sample();

OrderResult result = orderService.processWithSpecialDiscount(bulkOrder, vipCustomer);

assertThat(result.getDiscount()).isGreaterThanOrEqualTo(bulkOrder.getTotalAmount() * 0.2);
}

5. Configure Once, Reuse Everywhere

  • Create specialized fixture configurations: Define common configurations once and reuse them.
// Define common configurations
public class TestFixtures {
public static final FixtureMonkey TEST_FIXTURE_MONKEY = FixtureMonkey.builder()
.nullInject(0.0) // No null values
.build();

public static ArbitraryBuilder<User> validUser() {
return TEST_FIXTURE_MONKEY.giveMeBuilder(User.class)
.set("email", "test@example.com")
.set("active", true);
}
}

// Use in tests
@Test
void testUserRegistration() {
User user = TestFixtures.validUser().sample();

userService.register(user);

assertThat(userRepository.findByEmail(user.getEmail())).isNotNull();
}

6. Test Edge Cases and Boundary Conditions

  • Generate test cases with boundary values: Test min/max values and edge cases.
@Test
void testUnderageUserCannotAccessAdultContent() {
// Test with underage user
User underage = fixtureMonkey.giveMeBuilder(User.class)
.set("age", 17) // Just below legal age
.sample();

assertThat(userService.canAccessAdultContent(underage)).isFalse();
}

@Test
void testOfAgeUserCanAccessAdultContent() {
// Test with exactly of-age user
User ofAge = fixtureMonkey.giveMeBuilder(User.class)
.set("age", 18) // Exactly legal age
.sample();

assertThat(userService.canAccessAdultContent(ofAge)).isTrue();
}

7. Make Tests Reproducible

  • Use a fixed seed for tests that need reproducibility: This ensures consistent test results.
@Test
@Seed(123L) // Makes the test reproducible
void testComplexBehavior() {
List<Order> orders = fixtureMonkey.giveMe(Order.class, 100);

OrderSummary summary = orderService.summarize(orders);

assertThat(summary.getTotalAmount()).isGreaterThan(0);
}

8. Define Type-Specific Generation Rules

  • Register custom rules for specific types: Define how types should be generated consistently across all tests.
  • When to use: Use this approach when you need to control how a specific type is generated everywhere it appears in your tests.
// Create custom Fixture Monkey with type-specific generation rules
FixtureMonkey fixtureMonkey = FixtureMonkey.builder()
// Register a custom value for a simple type
.register(String.class, it -> it.giveMeBuilder("custom-string"))

// Register a custom rule for Email type
// This affects ALL Email instances created by this fixture monkey
.register(Email.class, fixture -> fixture.giveMeBuilder(new Email("test@example.com")))

// Register a custom rule for a complex type with validation
// Applies these rules whenever User instances are created
.register(User.class, fixture -> fixture
.setPostCondition(user -> user.getAge() >= 18)
.set("status", "ACTIVE"))

// Register a factory method for a type
.register(Product.class, fixture -> fixture
.instantiate(factoryMethod("createDefault")
.parameter(String.class, "productName"))
.set("productName", "Standard Product"))

.build();

// Using the custom registered instance
String customString = fixtureMonkey.giveMeOne(String.class); // Returns "custom-string"
Email email = fixtureMonkey.giveMeOne(Email.class); // Returns Email with "test@example.com"
User user = fixtureMonkey.giveMeOne(User.class); // Returns an adult user with ACTIVE status

9. Configure Field-Level Rules for Complex Objects

  • Define rules for individual fields within objects: Customize how individual properties are generated within complex objects.
  • When to use: Use this approach when you need fine-grained control over individual fields within a complex object, applying different rules to different properties within the same class.
FixtureMonkey fixtureMonkey = FixtureMonkey.builder()
// Register rules for each field in a complex type
// This provides field-by-field control, unlike the type-level register method
.registerGroup(ProductDetails.class, group -> group
// Each field can have its own specific generation rules
.register("name", Arbitraries.strings().alpha().ofMinLength(3).ofMaxLength(50))
.register("sku", Arbitraries.strings().numeric().ofLength(10))
.register("description", Arbitraries.strings().ofMinLength(10).ofMaxLength(500))
.register("inStock", true) // Always in stock for tests
.register("price", Arbitraries.doubles().between(1.0, 999.99))
.register("weight", Arbitraries.doubles().between(0.1, 100.0))
)
.build();

// The generated ProductDetails will have each field following its specific rule
ProductDetails product = fixtureMonkey.giveMeOne(ProductDetails.class);
// name - alphabetic string between 3-50 chars
// sku - numeric string of exactly 10 chars
// description - any string between 10-500 chars
// inStock - always true
// price - between 1.0 and 999.99
// weight - between 0.1 and 100.0