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:
- ComponentTesterSpec: For execution testing of components, functions, and workflows
- 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:
- Using the same component model: Tests use the same
Component and DSL structures
- Leveraging existing topics: Uses the same execution request/response topics
- Supporting all DSL features: Functions, workflows, routes, validation, etc.
- 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.