Writing tests

Writing tests

Tests are written using Kotlin, following Kotest’s Describe spec style.

  • Tests are placed anywhere under the test/ directory of your Taxi project.
  • Test files should be named with a suffix of either Test.kt or Spec.kt

A custom base class of OrbitalSpec is provided, which provides several convenience functions to make testing easier:

import com.orbitalhq.preflight.dsl.OrbitalSpec
import io.kotest.matchers.shouldBe
 
class PersonTest : OrbitalSpec({
    describe("Simple tests") {
        it("returning a scalar") {
            """find { 1 + 2 }""".queryForScalar()
                .shouldBe(3)
        }
    }
})

Compiling your Taxi project

By default, the taxi project is compiled before any tests are run. If compilation fails, the tests are not executed.

The Taxi project is found by reading the taxi.conf file in the root of your Taxi project.

Test helpers

The OrbitalSpec base class provides a suite of query helper methods designed to make writing tests for Taxi/Orbital projects faster and more expressive.

These helpers let you write and run TaxiQL queries directly within your tests and inspect the results in a Kotlin-native way.

Query helper methods

All helper methods are defined as Kotlin Extension functions on the String class, which means you can write queries like this:

"find { 1 + 2 }".queryForScalar() // returns 3

This makes it easy to inline small queries directly in your test cases.

queryForScalar

queryForScalar(): Any?

Executes the query and returns the first result as a raw scalar value (e.g., Int, String, Boolean). Use this for queries that return a single value, like a literal or a single field projection.

"find { 6 * 7 }".queryForScalar() shouldBe 42

queryForObject

queryForObject():Map<String,Any?>

Executes the query and returns the first result as a map. Each field in the result is represented as a key-value pair in the returned Map.

Use this when your query returns an object or a record.

it("returns a map when using queryForObject") {
    """given { Person = { id: 123, age: 12 } }
        find { PossibleAdult }
        """.queryForObject()
        .shouldBe(
            mapOf(
                "id" to 123,
                "age" to 12,
                "isAdult" to false
            )
        )
}

queryForCollection

queryForCollection():List<Map<String,Any?>>

Executes the query and returns a collection of objects, each represented as a Map<String, Any?>.

Use this when your query returns a list or collection of values.

it("returns a list of maps when using queryForCollection") {
        """
        find { [ { name: 'Alice' }, { name: 'Bob' } ] }
        """.queryForCollection()
        .shouldBe(
            listOf(
                mapOf("name" to "Alice"),
                mapOf("name" to "Bob")
            )
        )
}

queryForTypedInstance

queryForTypedInstance():TypedInstance

This allows for deeper inspection, such as:

  • Access to field-level types
  • Provenance/lineage tracking
  • Evaluation errors and unresolved values

Useful in lower-level tests where you want to assert not just values, but typing behavior or error diagnostics.

Stubbing service calls

Each query method supports stubbing external data sources in two ways:

1. Using Stub Scenarios (Recommended)

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

2. Using Stub Customizer (Advanced)

it("advanced stub customization") {
    """find { Person(PersonId == 1) }""".queryForObject { stubService ->
        // Fine-grained control over stub configuration
        stub.addResponse("getPerson", """{ "id" : 123, "age" : 36 }""")
    }
}

The stub scenarios approach is more convenient for most use cases, while the stub customizer allows for things like

  • controlling responses based on inputs
  • streaming responses (for faking Kafka topics, etc)
  • Throwing errors / simulating failures

For more information, read the dedicated docs on Stubbing responses

Direct Orbital API Access

For advanced use cases, you can access the underlying Orbital instance directly using the orbital() method. This provides full access to Orbital’s query engine and configuration:

it("should access Orbital API directly") {
    val orbital = orbital()
    
    // Access the compiled schema
    val schema = orbital.schema
    val userType = schema.type("User")
    
    // Execute raw queries with full control
    val queryResult = orbital.query("""
        find { Person(PersonId == "123") }
    """)
    
    // Access Orbital's internal services
    val operationInvoker = orbital.operationInvoker
    val schemaProvider = orbital.schemaProvider
}

Advanced Query Execution

Use direct Orbital access for complex query scenarios:

it("should execute complex queries with Orbital API") {
    val orbital = orbital()
    
    // Execute queries with custom context
    val queryContext = QueryContext.builder()
        .withParameter("userId", "user123")
        .build()
        
    val result = orbital.query("""
        given { userId : UserId = parameter("userId") }
        find { User(id == userId) }
    """, queryContext)
    
    // Access detailed query results
    result.results.forEach { instance ->
        println("Type: ${instance.type}")
        println("Value: ${instance.value}")
        println("Lineage: ${instance.dataSource}")
    }
}

Schema Inspection and Validation

Access schema metadata for advanced testing scenarios:

it("should validate schema structure") {
    val orbital = orbital()
    val schema = orbital.schema
    
    // Inspect types and their properties
    val userType = schema.type("User")
    userType.fields.forEach { field ->
        println("Field: ${field.name}, Type: ${field.type}")
    }
    
    // Validate service definitions
    val services = schema.services
    services.forEach { service ->
        println("Service: ${service.name}")
        service.operations.forEach { operation ->
            println("  Operation: ${operation.name}")
        }
    }
}

Custom Operation Execution

For testing specific operations or implementing custom behavior:

it("should execute operations directly") {
    val orbital = orbital()
    
    // Execute a specific operation by name
    val operation = orbital.schema.operation("getUserById")
    val parameters = mapOf("id" to TypedValue.of("user123"))
    
    val result = orbital.operationInvoker.invoke(
        operation,
        parameters,
        QueryContext.empty()
    )
    
    // Process the raw result
    result.fold(
        { error -> fail("Operation failed: $error") },
        { value -> value.value shouldBe expectedValue }
    )
}

When to Use Direct API Access

Use orbital() when you need to:

  • Inspect compiled schema metadata
  • Execute queries with custom parameters or context
  • Test Orbital’s internal behavior or configuration
  • Implement custom query execution logic
  • Debug complex compilation or execution issues

For most testing scenarios, the convenience methods (queryForScalar, queryForObject, etc.) are recommended as they provide a simpler API.