Stubbing responses

Stubbing responses

The StubService is a powerful testing utility that allows you to mock external service calls and dependencies in your Orbital/Taxi tests. It provides a flexible and intuitive API for configuring responses, tracking invocations, and testing complex scenarios without requiring actual external services.

Overview

StubService enables you to:

  • Mock external dependencies - Replace real service calls with controlled responses
  • Test error scenarios - Simulate failures and edge cases
  • Track invocations - Verify that operations are called with expected parameters

Simple usage

When all you need to do is provide a simple response to an operation call, you can use the convenience method:

it("should let me customize a stub return value") {
    """
    find { Person(PersonId == 1) } as PossibleAdult
    """.queryForObject(
        stub("getPerson").returns("""{ "id" : 123, "age" : 36 }"""),
        stub("getAdulting").returns("""{ "isAdult" : true }""") // you can specify multiple stubbed responses
    )
        .shouldBe(mapOf(
            "id" to 123,
            "age" to 36,
            "isAdult" to true
        ))
}

Basic Usage Patterns

Simple JSON Responses

The most common pattern is providing JSON-based responses:

it("should return user data") {
    """find { Person(PersonId == 1) }""".queryForObject { stubService ->
        stubService.addResponse("getPerson", """
            {
                "id": 123,
                "name": "John Doe",
                "email": "john@example.com",
                "age": 30
            }
        """)
    }
}

Array Responses

For operations that return collections:

it("should return multiple users") {
    """find { Person }""".queryForCollection { stubService ->
        stubService.addResponse("getAllPersons", """
            [
                {"id": 1, "name": "Alice", "age": 25},
                {"id": 2, "name": "Bob", "age": 35},
                {"id": 3, "name": "Charlie", "age": 45}
            ]
        """)
    }
}

Advanced Response Configuration

Parameter-Based Responses

Configure different responses based on input parameters:

it("should return different users based on role") {
    """find { Person(role == "admin") }""".queryForObject { stubService ->
        stubService.addResponsesByParameter(
            "getPersonByRole",
            mapOf(
                "admin" to """{"id": 1, "name": "Admin User", "permissions": ["ALL"]}""",
                "user" to """{"id": 2, "name": "Regular User", "permissions": ["READ"]}""",
                "guest" to """{"id": 3, "name": "Guest", "permissions": []}"""
            )
        )
    }
}

Dynamic Response Logic

For complex conditional logic:

it("should handle dynamic responses") {
    """find { Order(status == "ACTIVE") }""".queryForCollection { stubService ->
        stubService.addResponse("getOrdersByStatus") { operation, parameters ->
            val status = parameters.first().second.value as String
            when (status) {
                "ACTIVE" -> listOf(
                    activeOrder1.right(),
                    activeOrder2.right()
                )
                "COMPLETED" -> listOf(completedOrder.right())
                "CANCELLED" -> emptyList()
                else -> throw IllegalArgumentException("Unknown status: $status")
            }
        }
    }
}

Table-Based Operations

Convenient method for table findMany operations:

it("should stub table operations") {
    """find { User }""".queryForCollection { stubService ->
        // Automatically configures "users_findManyUser" operation
        stubService.addTableFindManyResponse("users", """
            [
                {"id": 1, "username": "alice", "active": true},
                {"id": 2, "username": "bob", "active": false}
            ]
        """)
    }
}

Data Source Tracking and Lineage

Enable data source tracking to understand where data comes from in your queries by passing modifyDataSource = true in the query.

Data lineage is available on the dataSource property of all TypedInstance objects. To access it, you must use the queryForTypedInstance() method:

it("should track data lineage") {
    """find { Person(PersonId == 1) }""".queryForTypedInstace { stubService ->
        stubService.addResponse(
            "getPerson",
            """{"id": 123, "name": "John"}""",
            modifyDataSource = true  // Enables lineage tracking
        )
    }
    // Results will include operation metadata for debugging and analysis
}

Error Testing

Simulating Service Failures

Test how your code handles external service failures:

it("should handle service failures gracefully") {
    assertThrows<RuntimeException> {
        """find { Person(PersonId == 1) }""".queryForObject { stubService ->
            stubService.addResponseThrowing(
                "getPerson",
                RuntimeException("Service temporarily unavailable")
            )
        }
    }
}
 
it("should handle validation errors") {
    """find { Person }""".queryForCollection { stubService ->
        stubService.addResponseThrowing(
            "createPerson",
            IllegalArgumentException("Invalid email format")
        )
    }
}

Streaming and Flow Responses

For operations that return streaming data:

it("should handle streaming responses") {
    """find { PriceUpdate }""".queryForCollection { stubService ->
        stubService.addResponseFlow("subscribeToPrices") { operation, parameters ->
            flow {
                repeat(3) { i ->
                    val price = TypedInstance.from(
                        priceType,
                        mapOf("symbol" to "AAPL", "price" to (150.0 + i)),
                        schema
                    )
                    emit(price.right())
                    delay(100) // Simulate real-time updates
                }
            }
        }
    }
}

Invocation Tracking and Verification

Counting Operation Calls

Verify that operations are called the expected number of times:

it("should track operation invocations") {
    val result = """find { Person(PersonId == 1) }""".queryForObject { stubService ->
        stubService.addResponse("getPerson", """{"id": 123, "name": "John"}""")
    }
 
    // Verify the operation was called exactly once
    stubService.callCount("getPerson") shouldBe 1
 
    // Verify other operations weren't called
    stubService.callCount("deletePerson") shouldBe 0
}

Parameter Verification

Check what parameters were passed to operations:

it("should verify operation parameters") {
    """find { Person(PersonId == 123) }""".queryForObject { stubService ->
        stubService.addResponse("getPerson", """{"id": 123, "name": "John"}""")
    }
 
    // Check the parameters that were passed
    val invocations = stubService.invocations["getPerson"]!!
    invocations.first().value shouldBe 123
}

Wildcard and Auto-Mock Responses

For rapid prototyping or query plan generation, enable automatic responses:

it("should auto-generate responses for all operations") {
    val (vyne, stubService) = StubService.stubbedVyne(schema)
 
    // Enable automatic mock responses for ANY operation
    stubService.returnStubValuesForAllOperations()
 
    // Now any query will work without explicit configuration
    val users = vyne.query("find { User }")
    val products = vyne.query("find { Product where category == 'electronics' }")
 
    // You can still override specific operations
    stubService.addResponse("getSpecialUser", specificUserData)
}

Method Chaining and Fluent API

StubService supports method chaining for clean test setup:

it("should support method chaining") {
    """find { UserWithProfile }""".queryForObject { stubService ->
        stubService
            .addResponse("getUser", """{"id": 1, "name": "John"}""")
            .addResponse("getUserProfile", """{"userId": 1, "bio": "Developer"}""")
            .addResponse("getUserPreferences", """{"theme": "dark", "language": "en"}""")
    }
}

Clean-up and State Management

Each new test scenario in Preflight will create a clean stub service.

However, if you need to clean state within a single test, you can use the clearXxx methods:

Clearing State within a test

it("should clean up between test cases") {
    """find { Person }""".queryForCollection { stubService ->
        // Clear all responses and invocation history
        stubService.clearAll()
 
        // Or clear just invocations (keeping responses)
        stubService.clearInvocations()
 
        // Or clear just responses (keeping invocation history)
        stubService.clearHandlers()
 
        stubService.addResponse("getAllPersons", personListJson)
    }
}