Skip to main content

Documentation Index

Fetch the complete documentation index at: https://platform.docs.zenoo.com/llms.txt

Use this file to discover all available pages before exploring further.

Component Tester Documentation

Overview

The Component Tester is a testing framework designed to facilitate unit testing of Zenoo Hub components. It provides a comprehensive testing environment that simulates the execution of components, workflows, and functions in an isolated testing context using Kafka Streams test utilities.

Key Features

  • Isolated Testing Environment: Uses Kafka Streams TopologyTestDriver for testing without external dependencies
  • Component Testing: Test individual components, functions, and workflows
  • Component Validation: Comprehensive validation testing for component definitions, configurations, and HTTP workflows
  • Mock Support: Mock component dependencies and external services
  • File Upload Testing: Test file upload functionality with mock file cache
  • Dependency Management: Test components with their dependencies
  • Spock Framework Integration: Built on Spock for expressive test specifications

Architecture

The Component Tester consists of several core classes:

Core Classes

ComponentTester

The main testing engine that manages the Kafka Streams test environment. Key Responsibilities:
  • Manages Kafka Streams TopologyTestDriver
  • Handles execution request/response topics
  • Provides file upload capabilities
  • Stores components in mock repository
Key Methods:
  • request(UUID, ExecutionRequest): Sends execution requests
  • executionResponseFor(ExecutionRequestEvent): Retrieves execution responses
  • upload(UUID, InputStream): Uploads files for testing
  • storeComponent(Component): Stores components in mock repository

ComponentTest

Represents a test instance for a specific component execution. Key Methods:
  • execute(payload): Executes a function or workflow
  • getResult(): Retrieves the execution result
  • getRoute(): Gets the current route information
  • getCurrentRoute(): Queries the current route state
  • submit(payload): Submits data to the current route
  • upload(Resource): Uploads a file resource

ComponentTestBuilder

Fluid builder API for creating component tests. Key Methods:
  • function(String name): Sets up a function test
  • workflow(String name): Sets up a workflow test
  • component(Component): Sets the component to test
  • component(ComponentFactory): Sets the component to test using ComponentFactory
  • dependency(Component): Adds component dependencies
  • mock(Closure): Adds mock behavior
  • config(payload): Sets component configuration

ComponentValidator

Validation engine for testing component definitions and configurations. Key Methods:
  • validateDefinition(Component): Validates component DSL structure and syntax
  • validateConfig(Component): Validates component configuration requirements

ComponentValidatorBuilder

Builder API for creating component validation tests. Key Methods:
  • component(Component): Sets the component to validate
  • config(Map): Sets component configuration for validation

Usage Examples

The Component Tester provides two main testing approaches:
  1. ComponentTesterSpec: For execution testing of components, functions, and workflows
  2. ComponentValidatorSpec: For validation testing of component definitions and configurations

ComponentTesterSpec - Execution Testing

ComponentTesterSpec is used for testing the actual execution behavior of components, including functions, workflows, file uploads, and dependency interactions.

Basic Function Testing

class MyFunctionSpec extends ComponentTesterSpec {

    static component = ComponentBuilder.component('my-component:v1') {
        function("hello") { name ->
            success(message: "Hello, ${name}!")
        }
    }

    def "should execute hello function"() {
        given:
        def test = testBuilder()
                .component(component)
                .function('hello')
                .build()

        when:
        test.execute("World")

        then:
        test.getResult().payload.message == "Hello, World!"
    }
}

Using ComponentFactory with testBuilder

class MyFunctionWithFactorySpec extends ComponentTesterSpec {

    def "should execute hello function using ComponentFactory"() {
        given:
        def test = testBuilder(MyComponentFactory)
                .function('hello')
                .build()

        when:
        test.execute("World")

        then:
        test.getResult().payload.message == "Hello, World!"
    }

    static class MyComponentFactory implements ComponentFactory {
        @Override
        Component getComponent() {
            return ComponentBuilder.component('my-component:v1') {
                function("hello") { name ->
                    success(message: "Hello, ${name}!")
                }
            }
        }
    }
}

Workflow Testing

class MyWorkflowSpec extends ComponentTesterSpec {

    static component = ComponentBuilder.component('workflow:v1') {
        workflow('user-registration') {
            route('validate') {
                uri '/validate'
                namespace user
                validate {
                    required('email', 'password')
                }
            }

            function('process') {
                input user
                namespace result
            }

            route('complete') {
                uri '/complete'
                export result
                terminal()
            }
        }
    }

    def "should execute user registration workflow"() {
        given:
        def test = testBuilder()
                .component(component)
                .workflow('user-registration')
                .build()

        when:
        test.execute([email: 'test@example.com', password: 'secret'])

        then:
        test.currentRoute.uri == '/validate'
        test.submit([email: 'test@example.com', password: 'secret'])
        test.route.uri == '/complete'
    }
}

Mocking Dependencies

class MockedComponentSpec extends ComponentTesterSpec {

    static component = ComponentBuilder.component('api-client:v1') {
        function("fetch-data") {
            http {
                url "https://api.example.com/data"
                method "GET"
            }
        }
    }

    def "should mock HTTP calls"() {
        given:
        def test = testBuilder()
                .component(component)
                .function('fetch-data')
                .mock {
                    http {
                        url "https://api.example.com/data"
                        response {
                            status 200
                            body [data: "mocked response"]
                        }
                    }
                }
                .build()

        when:
        test.execute()

        then:
        test.getResult().payload.data == "mocked response"
    }
}

Testing with Dependencies

class DependencySpec extends ComponentTesterSpec {

    static dependency = ComponentBuilder.component('dependency:v1') {
        function("process") { input ->
            success(processed: input.toUpperCase())
        }
    }

    static component = ComponentBuilder.component('main:v1') {
        dependencies {
            component 'dependency'
        }

        function("main") { input ->
            function('dependency@process') {
                input input
            }
        }
    }

    def "should execute with dependency"() {
        given:
        def test = testBuilder()
                .component(component)
                .dependency(dependency)
                .function('main')
                .build()

        when:
        test.execute("hello")

        then:
        test.getResult().payload.processed == "HELLO"
    }
}

File Upload Testing

class FileUploadSpec extends ComponentTesterSpec {

    @Value("classpath:files/test-image.jpg")
    Resource testImage

    static component = ComponentBuilder.component('upload:v1') {
        workflow('upload') {
            route('upload') {
                uri '/upload'
                namespace file
                validate {
                    file()
                }
            }

            function('process') {
                input file
                namespace result
            }
        }
    }

    def "should handle file upload"() {
        given:
        def test = testBuilder()
                .component(component)
                .workflow('upload')
                .build()

        when:
        test.execute()
        def descriptor = test.upload(testImage)
        test.submit(descriptor)

        then:
        test.getResult().payload.descriptor == descriptor
    }
}

ComponentValidatorSpec - Validation Testing

ComponentValidatorSpec is used for testing component definitions, configurations, and DSL structure without executing the components. This is useful for ensuring components are properly structured and configured before deployment.

Basic Definition Validation

class ComponentValidationSpec extends ComponentValidatorSpec {

    def "should validate simple workflow component"() {
        given:
        def component = ComponentBuilder.component('simple:v1') {
            workflow('main') {
                route('start') {
                    uri '/start'
                    terminal()
                }
            }
        }

        when:
        def validatorTest = validatorBuilder()
                .component(component)
                .build()

        then:
        assertDefinitionValid(validatorTest)
    }

    def "should fail validation when function is missing"() {
        given:
        def component = ComponentBuilder.component('invalid:v1') {
            workflow('main') {
                function('nonExistentFunction')

                route('result') {
                    uri '/result'
                    terminal()
                }
            }
        }

        when:
        def validatorTest = validatorBuilder()
                .component(component)
                .build()

        then:
        assertDefinitionInvalid(validatorTest)
    }
}

Using ComponentFactory with validatorBuilder

class ComponentValidationWithFactorySpec extends ComponentValidatorSpec {

    def "should validate component using ComponentFactory"() {
        when:
        def validatorTest = validatorBuilder(SimpleComponentFactory)
                .build()

        then:
        assertDefinitionValid(validatorTest)
    }

    def "should validate component with config using ComponentFactory"() {
        when:
        def validatorTest = validatorBuilder(ConfigurableComponentFactory)
                .config([
                    username: 'testuser',
                    password: 'secret123'
                ])
                .build()

        then:
        assertDefinitionValid(validatorTest)
        assertConfigValid(validatorTest)
    }

    static class SimpleComponentFactory implements ComponentFactory {
        @Override
        Component getComponent() {
            return ComponentBuilder.component('simple:v1') {
                workflow('main') {
                    route('start') {
                        uri '/start'
                        terminal()
                    }
                }
            }
        }
    }

    static class ConfigurableComponentFactory implements ComponentFactory {
        @Override
        Component getComponent() {
            return ComponentBuilder.component('configurable:v1') {
                require {
                    username
                    password
                }

                workflow('main') {
                    route('start') {
                        uri '/start'
                        terminal()
                    }
                }
            }
        }
    }
}

Configuration Validation

class ComponentConfigValidationSpec extends ComponentValidatorSpec {

    def "should validate component with required config"() {
        given:
        def component = ComponentBuilder.component('config-required:v1') {
            require {
                username
                password
                apiEndpoint
            }

            workflow('main') {
                route('start') {
                    uri '/start'
                    terminal()
                }
            }
        }

        when:
        def validatorTest = validatorBuilder()
                .component(component)
                .config([
                    username: 'testuser',
                    password: 'secret123',
                    apiEndpoint: 'https://api.example.com'
                ])
                .build()

        then:
        assertDefinitionValid(validatorTest)
        assertConfigValid(validatorTest)
    }

    def "should fail validation when required config is missing"() {
        given:
        def component = ComponentBuilder.component('config-missing:v1') {
            require {
                username
                password
                apiEndpoint
            }

            workflow('main') {
                route('start') {
                    uri '/start'
                    terminal()
                }
            }
        }

        when:
        def validatorTest = validatorBuilder()
                .component(component)
                .build()

        then:
        assertDefinitionValid(validatorTest)
        assertConfigInvalid(validatorTest)
    }
}

HTTP Workflow Validation

class ComponentHttpValidationSpec extends ComponentValidatorSpec {

    def "should validate HTTP workflow with multiple handlers"() {
        given:
        def component = ComponentBuilder.component('http-workflow:v1') {
            workflow('main') {
                http {
                    url 'http://api.example.com'
                    timeout 3000
                }.onStatus(200) {
                    route('success') {
                        uri '/success'
                        terminal()
                    }
                }.onStatus(400) {
                    route('error') {
                        uri '/error'
                        terminal()
                    }
                }.onError {
                    route('system-error') {
                        uri '/system-error'
                        terminal()
                    }
                }
            }
        }

        when:
        def validatorTest = validatorBuilder()
                .component(component)
                .build()

        then:
        assertDefinitionValid(validatorTest)
    }

    def "should validate HTTP in function context"() {
        given:
        def component = ComponentBuilder.component('http-function:v1') {
            function('callApi') {
                http {
                    url 'http://api.example.com'
                }.onSuccess {
                    success()
                }.onError {
                    error("API call failed")
                }
            }

            workflow('main') {
                function('callApi')

                route('result') {
                    uri '/result'
                    terminal()
                }
            }
        }

        when:
        def validatorTest = validatorBuilder()
                .component(component)
                .build()

        then:
        assertDefinitionValid(validatorTest)
    }
}

Validation Testing Features

The Component Validator supports testing of:
  • Definition Validation: DSL syntax, function references, workflow structure
  • Configuration Validation: Required config fields, typed constraints, nested configurations
  • HTTP Validation: HTTP blocks, status handlers, response handlers, timeout configuration
  • Dependency Validation: Component and connector dependencies
  • Complex Scenarios: Multi-step workflows, conditional logic, error handling

Validation Test Base Classes

The framework provides specialized base classes for validation testing:
  • ComponentValidatorSpec: Base class for all validation tests

Testing Best Practices

1. Choose the Right Base Class

  • ComponentTesterSpec: For execution testing (functions, workflows, file uploads, dependencies)
  • ComponentValidatorSpec: For validation testing (component definitions, configurations)
// For execution testing
class MyExecutionSpec extends ComponentTesterSpec {
    // Test actual component execution
}

// For validation testing
class MyValidationSpec extends ComponentValidatorSpec {
    // Test component structure and configuration
}

2. Use the Builder Pattern

The framework provides two builder patterns:

Direct Component Usage

// For ComponentTesterSpec
def test = testBuilder()
        .component(component)
        .function('my-function')
        .config([key: 'value'])
        .build()

// For ComponentValidatorSpec
def validatorTest = validatorBuilder()
        .component(component)
        .config([key: 'value'])
        .build()

ComponentFactory Usage

// For ComponentTesterSpec
def test = testBuilder(MyComponentFactory)
        .function('my-function')
        .config([key: 'value'])
        .build()

// For ComponentValidatorSpec
def validatorTest = validatorBuilder(MyComponentFactory)
        .config([key: 'value'])
        .build()

3. Mock External Dependencies

Mock external services and dependencies to ensure isolated testing:
.mock {
    http {
        url "https://external-api.com"
        response {
            status 200
            body [result: "success"]
        }
    }
}

4. Test Different Execution Types

Test various execution scenarios:
  • Functions: Direct execution with input/output validation
  • Workflows: Multi-step execution with route transitions
  • File Uploads: File handling and processing
  • Dependencies: Component interaction testing
  • Validation: Component definition and configuration validation

Component Validation Testing

Use the validation framework to test component structure without execution:
// Direct component usage
def test = validatorBuilder()
        .component(component)
        .config([key: 'value'])
        .build()

assertDefinitionValid(test)
assertConfigValid(test)

// ComponentFactory usage
def factoryTest = validatorBuilder(MyComponentFactory)
        .config([key: 'value'])
        .build()

assertDefinitionValid(factoryTest)
assertConfigValid(factoryTest)

Dependencies

The Component Tester includes the following key dependencies:
  • Spring Boot Test: Testing framework integration
  • Kafka Streams Test Utils: Kafka Streams testing utilities
  • Spock Framework: Testing specification framework
  • Groovy: Dynamic language support
  • Reactor Test: Reactive testing utilities

Integration with Main Framework

The Component Tester integrates seamlessly with the main Zenoo Hub framework by:
  1. Using the same component model: Tests use the same Component and DSL structures
  2. Leveraging existing topics: Uses the same execution request/response topics
  3. Supporting all DSL features: Functions, workflows, routes, validation, etc.
  4. Mocking framework components: Provides mocks for all major framework components
This ensures that tests accurately reflect real-world component behavior while providing the isolation and control needed for effective unit testing.