Skip to content

Conversation

@tigermint
Copy link
Contributor

@tigermint tigermint commented Jun 7, 2025

Summary

This PR implements a Cursor IDE rule file for FixtureMonkey test writing guidelines to help developers write more
effective and consistent tests using FixtureMonkey patterns and best practices. (#1190)

(Optional): Description

Added cursor/rules/fixture-monkey-test-writing-rule.mdc that provides intelligent code suggestions for FixtureMonkey
usage, including:

  • Method selection guidance (giveMeOne() vs giveMeBuilder() vs giveMe())
  • FixtureMonkey instance configuration best practices
  • Object customization with .set(), .size(), and builder methods
  • Support for complex objects, generic types, and collections
  • Context-aware suggestions based on existing code patterns

How Has This Been Tested?

As required by issue #1190, this rule has been extensively tested across different project

1. Spring Boot REST API Project

Scenario: Simple POJO object creation
Test Cases: User service testing with basic field customization
Results: ✅ Proper giveMeBuilder() suggestions, correct .set() usage guidance

Before (without the rule):

@Test
void testSaveUser() {
    // given
    User user = new User();
    user.setName("John Doe");
    user.setAge(30);
    user.setEmail("john@example.com");
    user.setPhoneNumber("123-456-7890");

    // when
    User savedUser = userService.save(user);

    // Test logic...
}

@Test
void testFindUser() {
    // given
    User user1 = new User();
    user1.setName("John Doe");
    user1.setAge(30);
    user1.setEmail("john@example.com");
    user1.setPhoneNumber("123-456-7890");

    User user2 = new User();
    user2.setName("Jane Doe");
    user2.setAge(25);
    user2.setEmail("jane@example.com");
    user2.setPhoneNumber("098-765-4321");

    userService.save(user1);
    userService.save(user2);

    // when
    User foundUser = userService.findById(1L);

    // Test logic...
}

After (with Cursor rule guidance):

@Test
void testSaveUser() {
    // given
    User user = fixtureMonkey.giveMeBuilder(User.class)
            .set("name", "John Doe")
            .set("age", 30)
            .set("email", "john@example.com")
            .set("phoneNumber", "123-456-7890")
            .sample();

    // when
    User savedUser = userService.save(user);

    // Test logic...
}

@Test
void testFindUser() {
    // given
    User user = fixtureMonkey.giveMeBuilder(User.class)
            .set("name", "Jane Doe")
            .set("age", 25)
            .set("email", "jane@example.com")
            .set("phoneNumber", "987-654-3210")
            .sample();

    User savedUser = userService.save(user);
    Long userId = savedUser.getId();

    // when
    User foundUser = userService.findById(userId);

    // Test logic...
}

2. JPA Entities Complex Domain Model

Scenario: Complex entity relationships with @manytoone
Test Cases: Order/Customer/OrderItem entity creation
Results: ✅ Appropriate handling of entity relationships, .size() method suggestions for collections

Before (without the rule):

@Test
void testCreateOrderWithItems() {
    // given
    Order order = new Order(customer, LocalDateTime.now(), BigDecimal.ZERO, "CREATED");

    OrderItem item1 = new OrderItem("Product 1", 2, new BigDecimal("10.00"));
    OrderItem item2 = new OrderItem("Product 2", 1, new BigDecimal("15.00"));

    item1.setOrder(order);
    item2.setOrder(order);

    // Test logic...
}

@Test
void testCreateCustomerWithMultipleOrders() {
    // given
    Customer customer = new Customer("Jane", "Smith", "jane@example.com", "987-654-3210");

    Order order1 = new Order(customer, LocalDateTime.now(), new BigDecimal("100.00"), "COMPLETED");
    Order order2 = new Order(customer, LocalDateTime.now(), new BigDecimal("200.00"), "PROCESSING");

    // Test logic...
}

After (with Cursor rule guidance):

@Test
void testCreateOrderWithItems() {
    // given
    Order order = fixtureMonkey.giveMeBuilder(Order.class)
            .set("status", "PENDING")
            .sample();

    List<OrderItem> orderItems = fixtureMonkey.giveMeBuilder(OrderItem.class)
            .size("$", 3)
            .set("order", order)
            .sampleList(3);

    // Test logic...
}

@Test
void testCreateCustomerWithMultipleOrders() {
    // given
    Customer customer = fixtureMonkey.giveMeBuilder(Customer.class)
            .set("firstName", "John")
            .set("lastName", "Doe")
            .set("email", "john.doe@example.com")
            .sample();

    List<Order> orders = fixtureMonkey.giveMeBuilder(Order.class)
            .size("$", 2)
            .set("customer", customer)
            .set("status", "COMPLETED")
            .sampleList(2);

    // Test logic...
}

3. Microservice DTO Structures

Scenario: Complex DTOs with nested objects, Maps, and generic types
Test Cases: ProductResponse with CategoryDto, PriceInfo, Map<String, Object>
Results: ✅ Proper generic type handling, nested object customization guidance

Before (without the rule):

@Test
void testCreateComplexProductResponse() {
    // given
    CategoryDto category1 = new CategoryDto(1L, "Electronics", "ELEC", null);
    CategoryDto category2 = new CategoryDto(2L, "Smartphones", "PHONE", category1);

    List<DiscountRule> discountRules = Arrays.asList(
            new DiscountRule("PERCENTAGE", new BigDecimal("10.0"), "SUMMER_SALE"),
            new DiscountRule("FIXED", new BigDecimal("50.0"), "NEW_CUSTOMER")
    );

    PriceInfo priceInfo = new PriceInfo(
            new BigDecimal("999.99"),
            new BigDecimal("899.99"),
            new BigDecimal("849.99"),
            "USD",
            discountRules
    );

    InventoryInfo inventory = new InventoryInfo(100, 10, true, "WAREHOUSE-A");

    Map<String, Object> attributes = new HashMap<>();
    attributes.put("color", "Black");
    attributes.put("weight", 0.5);
    attributes.put("dimensions", Arrays.asList(10, 5, 1));
    attributes.put("isWaterproof", true);
    attributes.put("warrantyMonths", 24);

    // when
    ProductResponse product = new ProductResponse(
            1L,
            "Premium Smartphone",
            "Latest model with advanced features",
            Arrays.asList(category1, category2),
            priceInfo,
            attributes,
            Arrays.asList("new", "premium", "smartphone"),
            inventory
    );

    // Test logic...
}

@Test
void testCreateProductWithMultipleCategories() {
    //given
    CategoryDto parentCategory = new CategoryDto(1L, "Electronics", "ELEC", null);

    CategoryDto childCategory1 = new CategoryDto(2L, "Smartphones", "PHONE", parentCategory);
    CategoryDto childCategory2 = new CategoryDto(3L, "Tablets", "TAB", parentCategory);
    CategoryDto grandChildCategory = new CategoryDto(4L, "Android Phones", "ANDROID", childCategory1);

    //when
    ProductResponse product = new ProductResponse();
    product.setId(1L);
    product.setName("Android Smartphone");
    product.setCategories(Arrays.asList(parentCategory, childCategory1, childCategory2, grandChildCategory));

    // Test logic...
}

After (with Cursor rule guidance):

@Test
void testCreateComplexProductResponse() {
    // given
    ProductResponse product = fixtureMonkey.giveMeBuilder(ProductResponse.class)
            .set("id", 1L)
            .set("name", "Test Product")
            .set("description", "A test product description")
            .size("categories", 2)
            .set("categories[0].name", "Electronics")
            .set("categories[1].name", "Gadgets")
            .set("priceInfo.basePrice", new BigDecimal("100.00"))
            .set("priceInfo.currency", "USD")
            .set("attributes", Map.of(
                    "color", "Red",
                    "weight", 1.5,
                    "isNew", true,
                    "dimensions", List.of(10, 20, 30)
            ))
            .size("tags", 3)
            .set("tags[0]", "new")
            .set("tags[1]", "sale")
            .set("tags[2]", "featured")
            .set("inventory.availableQuantity", 100)
            .set("inventory.inStock", true)
            .sample();

    // Test logic...
}

@Test
void testCreateProductWithMultipleCategories() {
    // given
    CategoryDto parentCategory = fixtureMonkey.giveMeBuilder(CategoryDto.class)
            .set("id", 1L)
            .set("name", "Electronics")
            .set("code", "ELEC")
            .sample();

    CategoryDto childCategory1 = fixtureMonkey.giveMeBuilder(CategoryDto.class)
            .set("id", 2L)
            .set("name", "Smartphones")
            .set("code", "PHONE")
            .set("parentCategory", parentCategory)
            .sample();

    CategoryDto childCategory2 = fixtureMonkey.giveMeBuilder(CategoryDto.class)
            .set("id", 3L)
            .set("name", "Laptops")
            .set("code", "LAPTOP")
            .set("parentCategory", parentCategory)
            .sample();

    // when
    ProductResponse product = fixtureMonkey.giveMeBuilder(ProductResponse.class)
            .set("id", 1L)
            .set("name", "Test Product")
            .set("categories", List.of(childCategory1, childCategory2))
            .sample();

    // Test logic...
}

4. Legacy Codebase with Manual Test Setup

Scenario: Existing complex manual object creation code
Test Cases: Employee objects with Address, ContactInfo, and complex Maps
Results: ✅ Successfully detected manual patterns and suggested FixtureMonkey alternatives

Before (without the rule):

@Test
void testEmployeeWithAddressAndContactInfo() {
    Long id = 1L;
    String firstName = "John";
    String lastName = "Doe";
    Date hireDate = new Date();
    String department = "Engineering";

    Address address = Address.builder()
            .street("123 Tech Street")
            .city("Seoul")
            .state("Gangnam")
            .zipCode("12345")
            .country("South Korea")
            .build();

    Map<String, String> socialMedia = new HashMap<>();
    socialMedia.put("linkedin", "john.doe");
    socialMedia.put("github", "johndoe");

    ContactInfo contactInfo = ContactInfo.builder()
            .email("john.doe@example.com")
            .phoneNumber("010-1234-5678")
            .emergencyContact("Jane Doe")
            .socialMedia(socialMedia)
            .build();

    List<String> skills = Arrays.asList("Java", "Spring", "JUnit");

    Map<String, Object> metadata = new HashMap<>();
    metadata.put("yearsOfExperience", 5);
    metadata.put("certifications", Arrays.asList("AWS", "Oracle"));
    metadata.put("projects", Arrays.asList("Project A", "Project B"));

    Employee employee = Employee.builder()
            .id(1L)
            .firstName("John")
            .lastName("Doe")
            .hireDate(new Date())
            .department("Engineering")
            .address(address)
            .contactInfo(contactInfo)
            .skills(skills)
            .metadata(metadata)
            .isActive(true)
            .build();

    // Test logic...

}

After (with Cursor rule guidance):

@Test
void testEmployeeWithAddressAndContactInfo() {
    Map<String, String> socialMedia = fixtureMonkey.giveMeBuilder(new TypeReference<Map<String, String>>() {
            })
            .size("$", 2)
            .sample();

    List<String> skills = fixtureMonkey.giveMeBuilder(new TypeReference<List<String>>() {
            })
            .size("$", 3)
            .sample();

    Map<String, Object> metadata = fixtureMonkey.giveMeBuilder(new TypeReference<Map<String, Object>>() {
            })
            .size("$", 3)
            .sample();

    Address address = fixtureMonkey.giveMeBuilder(Address.class)
            .sample();

    ContactInfo contactInfo = fixtureMonkey.giveMeBuilder(ContactInfo.class)
            .set("socialMedia", socialMedia)
            .sample();

    Employee employee = fixtureMonkey.giveMeBuilder(Employee.class)
            .set("address", address)
            .set("contactInfo", contactInfo)
            .set("skills", skills)
            .set("metadata", metadata)
            .sample();

    // Test logic...
}

5. Kotlin Data Class Projects

Scenario: Kotlin-specific features (nullable types, default values)
Test Cases: Product data classes with nullable fields and collections
Results: ✅ Proper handling of Kotlin nullability and default value features

Before (without the rule):

@Test
fun `test Order creation`() {
    val orderItem = OrderItem(
        productId = 1L,
        productName = "Test Product",
        quantity = 2,
        unitPrice = BigDecimal("19.99")
    )

    val shippingAddress = Address(
        street = "123 Test St",
        city = "Test City",
        state = "Test State",
        zipCode = "12345"
    )

    val paymentInfo = PaymentInfo(
        method = PaymentMethod.CREDIT_CARD,
        cardLastFour = "1234",  // nullable field
        transactionId = "TXN123",
        amount = BigDecimal("39.98")
    )

    val order = Order(
        id = 1L,
        customerId = 100L,
        items = listOf(orderItem),
        totalAmount = BigDecimal("39.98"),
        status = OrderStatus.PENDING,
        shippingAddress = shippingAddress,
        paymentInfo = paymentInfo
    )

    // Test logic...
}

After (with Cursor rule guidance):

private val fixtureMonkey = FixtureMonkey.builder()
    .plugin(KotlinPlugin())
    .build()

@Test
fun `test Order creation`() {
    val order = fixtureMonkey.giveMeBuilder<Order>()
        .size("items", 2)
        .set("items[0].productName", "Laptop")
        .set("items[0].quantity", 1)
        .set("items[0].unitPrice", BigDecimal("1500.00"))
        .set("items[1].productName", "Mouse")
        .set("items[1].quantity", 2)
        .set("items[1].unitPrice", BigDecimal("50.00"))
        .set("status", OrderStatus.PENDING)
        .set("shippingAddress.country", "KR")
        .set("paymentInfo.method", PaymentMethod.CREDIT_CARD)
        .set("paymentInfo.cardLastFour", "1234")
        .sample()

    // Test logic...
}

Is the Document updated?

We recommend that the corresponding documentation for this feature or change is updated within the pull request
If the update is scheduled for later, please specify and add the necessary information to
the discussion page.

@CLAassistant
Copy link

CLAassistant commented Jun 7, 2025

CLA assistant check
All committers have signed the CLA.

@seongahjo
Copy link
Contributor

Thank you for your contribution!

There are two things I would like to see improved.

First

The following conditions are used to select the introspector for use in the objectintrospector.

https://naver.github.io/fixture-monkey/v1-1-0/docs/generating-objects/introspector/#choosing-the-right-introspector-for-your-classes

Second

Replace the String path expression with exp (javaGetter, kotlin DSL Exp)

https://naver.github.io/fixture-monkey/v1-1-0/docs/customizing-objects/path-expressions/#type-safe-selection-with-javagetter

https://naver.github.io/fixture-monkey/v1-1-0/docs/plugins/kotlin-plugin/kotlin-exp/

Copy link
Contributor Author

@tigermint tigermint left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@seongahjo

Thank you for feedback!
I've added both improvements you requested.
All changes are based on the official documentation you referenced.

Comment on lines +9 to +67
## FixtureMonkey Instance Creation

### Basic Setup
```java
// Basic setup with default options
FixtureMonkey fixtureMonkey = FixtureMonkey.create();

// Kotlin setup with Kotlin plugin
val fixtureMonkey = FixtureMonkey
.plugin(KotlinPlugin())
.build();
```

### Advanced Configuration with Introspector Selection

Choose the appropriate introspector based on your class characteristics:

```java
// For classes with constructor parameters matching field names
FixtureMonkey fixtureMonkey = FixtureMonkey.builder()
.objectIntrospector(ConstructorPropertiesArbitraryIntrospector.INSTANCE)
.defaultNotNull(true)
.build();

// For classes with public fields
FixtureMonkey fixtureMonkey = FixtureMonkey.builder()
.objectIntrospector(FieldReflectionArbitraryIntrospector.INSTANCE)
.defaultNotNull(true)
.build();

// For classes with setter methods
FixtureMonkey fixtureMonkey = FixtureMonkey.builder()
.objectIntrospector(BeanArbitraryIntrospector.INSTANCE)
.defaultNotNull(true)
.build();

// For classes implementing Builder pattern
FixtureMonkey fixtureMonkey = FixtureMonkey.builder()
.objectIntrospector(BuilderArbitraryIntrospector.INSTANCE)
.defaultNotNull(true)
.build();

// For fail-safe approach (tries multiple introspectors)
FixtureMonkey fixtureMonkey = FixtureMonkey.builder()
.objectIntrospector(FailoverIntrospector.from(
ConstructorPropertiesArbitraryIntrospector.INSTANCE,
FieldReflectionArbitraryIntrospector.INSTANCE,
BeanArbitraryIntrospector.INSTANCE
))
.defaultNotNull(true)
.build();
```

#### Introspector Selection Guide:
- **ConstructorPropertiesArbitraryIntrospector**: Use when constructor parameters match field names (records, data classes)
- **FieldReflectionArbitraryIntrospector**: Use when class has public fields
- **BeanArbitraryIntrospector**: Use when class follows JavaBean conventions (getter/setter methods)
- **BuilderArbitraryIntrospector**: Use when class implements Builder pattern
- **FailoverIntrospector**: Use when you're unsure or need to support multiple class types
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added Introspector Selection Guidelines

  • Added comprehensive guide for choosing the right introspector based on class characteristics
  • Included ConstructorPropertiesArbitraryIntrospector, FieldReflectionArbitraryIntrospector, BeanArbitraryIntrospector, BuilderArbitraryIntrospector, and FailoverIntrospector

Comment on lines +112 to +164
### 1. Type-Safe Object Customization

**Java - Use javaGetter() for type-safe field selection:**
```java
// Setting specific values with type safety
fixtureMonkey.giveMeBuilder(Product.class)
.set(javaGetter(Product::getName), "Test Product")
.set(javaGetter(Product::getPrice), 100.0)
.sample();

// Setting collection sizes - Always set size before customizing elements
fixtureMonkey.giveMeBuilder(Order.class)
.size(javaGetter(Order::getItems), 3) // Set size first
.set(javaGetter(Order::getItems).index(0, Item::getName), "Item 1") // Then customize elements
.sample();

// Nested object customization
fixtureMonkey.giveMeBuilder(Order.class)
.set(javaGetter(Order::getCustomer, Customer::getName), "John Doe")
.set(javaGetter(Order::getItems).index(0, Item::getPrice), 100.0)
.sample();

// Using Arbitrary values
fixtureMonkey.giveMeBuilder(Product.class)
.set(javaGetter(Product::getPrice), Arbitraries.longs().greaterThan(0))
.sample();
```

**Kotlin - Use Kotlin DSL Exp for type-safe field selection:**
```kotlin
// Setting specific values with type safety
fixtureMonkey.giveMeBuilder<Product>()
.setExp(Product::name, "Test Product")
.setExp(Product::price, 100.0)
.sample()

// Setting collection sizes - Always set size before customizing elements
fixtureMonkey.giveMeBuilder<Order>()
.sizeExp(Order::items, 3) // Set size first
.setExp(Order::items["0"] into Item::name, "Item 1") // Then customize elements using Kotlin DSL
.sample()

// Nested object customization
fixtureMonkey.giveMeBuilder<Order>()
.setExp(Order::customer into Customer::name, "John Doe")
.setExp(Order::items["0"] into Item::price, 100.0)
.sample()

// Using Arbitrary values
fixtureMonkey.giveMeBuilder<Product>()
.setExp(Product::price, Arbitraries.longs().greaterThan(0))
.sample()
```
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replaced String Path Expressions with Type-Safe Alternatives

  • Java: Replaced all string expressions with javaGetter() method references
  • Kotlin: Used proper Kotlin DSL with setExp(), setExpGetter(), into, intoGetter

Comment on lines +349 to +363
7. **Using wrong introspector for class type**
```java
// Don't do this - using wrong introspector
FixtureMonkey fixtureMonkey = FixtureMonkey.builder()
.objectIntrospector(BeanArbitraryIntrospector.INSTANCE) // Wrong for record classes
.build();

// For record classes, use ConstructorPropertiesArbitraryIntrospector
public record User(String name, int age) {}

// Do this instead
FixtureMonkey fixtureMonkey = FixtureMonkey.builder()
.objectIntrospector(ConstructorPropertiesArbitraryIntrospector.INSTANCE) // Correct for records
.build();
```
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enhanced Anti-Patterns Section

  • Shows how using wrong introspector for class type can cause issues - e.g., using BeanArbitraryIntrospector for record classes instead of the correct ConstructorPropertiesArbitraryIntrospector

Comment on lines +367 to +372
1. What type of class are you working with?
- Record/Data class → Use `ConstructorPropertiesArbitraryIntrospector`
- Public fields → Use `FieldReflectionArbitraryIntrospector`
- JavaBean (getter/setter) → Use `BeanArbitraryIntrospector`
- Builder pattern → Use `BuilderArbitraryIntrospector`
- Mixed or unsure → Use `FailoverIntrospector`
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enhanced Decision Tree Section

  • Reorganized decision tree to prioritize introspector selection as the first step, ensuring users choose the right introspector before proceeding with other customizations

Comment on lines +399 to +405
8. Are you working with collections?
- Yes → Always set size before customizing elements using type-safe expressions
- No → Proceed with customization using type-safe expressions

9. Are you using Java or Kotlin?
- Java → Use `javaGetter()` for type-safe field selection
- Kotlin → Use Kotlin DSL with `setExp()`, `setExpGetter()`, `into`, `intoGetter` syntax
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enhanced Decision Tree Section

  • Added collection handling step emphasizing size-first approach with type-safe expressions
  • Added language-specific guidance for choosing between Java javaGetter() and Kotlin DSL (setExp(), setExpGetter(), into, intoGetter)

Copy link
Contributor

@seongahjo seongahjo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well done! It looks great!

Thank you for contributing!

@seongahjo seongahjo merged commit a66bd32 into naver:main Jun 13, 2025
8 checks passed
@tigermint tigermint deleted the add-cursor-rule branch October 21, 2025 15:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants