Plugins
BerryCrush provides a plugin system to extend its functionality. Plugins can hook into test execution, scenario, and step lifecycle events to perform custom actions like logging, reporting, or modifying behavior.
Plugin Architecture
All plugins implement the BerryCrushPlugin interface:
interface BerryCrushPlugin {
// Identity
val id: String get() = this::class.qualifiedName ?: "unknown"
val name: String get() = this::class.simpleName ?: "Unknown Plugin"
val priority: Int get() = 0
// Test execution lifecycle (called once per test run)
fun onTestExecutionStart() {}
fun onTestExecutionEnd() {}
// Scenario lifecycle
fun onScenarioStart(context: ScenarioContext) {}
fun onScenarioEnd(context: ScenarioContext, result: ScenarioResult) {}
// Step lifecycle
fun onStepStart(context: StepContext) {}
fun onStepEnd(context: StepContext, result: StepResult) {}
}
Lifecycle Events
Plugins receive callbacks at specific points during test execution:
Test Execution Level:
onTestExecutionStart(): Called once before the first scenario startsonTestExecutionEnd(): Called once after all scenarios complete
Scenario Level:
onScenarioStart(context): Called before each scenario beginsonScenarioEnd(context, result): Called after each scenario completes (with results)
Step Level:
onStepStart(context): Called before each step executesonStepEnd(context, result): Called after each step completes (with results)
Context Objects
ScenarioContext provides access to scenario execution state:
interface ScenarioContext {
val scenarioName: String // Name from scenario file
val scenarioFile: Path // Path to scenario file
val variables: MutableMap<String, Any> // Runtime variables
val metadata: Map<String, String> // Scenario metadata
val startTime: Instant // Execution start time
val tags: Set<String> // Scenario tags for filtering
}
StepContext provides access to step execution state:
interface StepContext {
val stepDescription: String // Full step description
val stepType: StepType // CALL, ASSERT, EXTRACT, CUSTOM
val stepIndex: Int // Zero-based index in scenario
val scenarioContext: ScenarioContext // Parent scenario
val request: HttpRequest? // Request details (CALL steps)
val response: HttpResponse? // Response details (after call)
val operationId: String? // OpenAPI operation ID
}
Result Objects
ScenarioResult contains the outcome of a scenario:
interface ScenarioResult {
val status: ResultStatus // PASSED, FAILED, SKIPPED, ERROR
val duration: Duration // Total execution time
val failedStep: Int // First failed step index (-1 if none)
val error: Throwable? // Exception if ERROR status
val stepResults: List<StepResult> // All step results
}
StepResult contains the outcome of a step:
interface StepResult {
val status: ResultStatus // PASSED, FAILED, SKIPPED, ERROR
val duration: Duration // Execution time
val failure: AssertionFailure? // Failure details if FAILED
val error: Throwable? // Exception if ERROR
}
Priority
Plugins execute in priority order (lower values execute first):
Negative priorities (e.g., -100): Setup/infrastructure plugins
Zero (default): Standard plugins (reporting, logging)
Positive priorities (e.g., 100): Cleanup/finalization plugins
Plugins with the same priority execute in registration order.
Creating a Custom Plugin
Basic Plugin
class LoggingPlugin : BerryCrushPlugin {
override val name = "logging"
override fun onScenarioStart(context: ScenarioContext) {
println("Starting scenario: ${context.scenarioName}")
}
override fun onScenarioEnd(context: ScenarioContext, result: ScenarioResult) {
println("Scenario ${context.scenarioName}: ${result.status}")
}
override fun onStepStart(context: StepContext) {
println(" Step: ${context.stepDescription}")
}
override fun onStepEnd(context: StepContext, result: StepResult) {
println(" Result: ${result.status}")
}
}
Stateful Plugin
Plugins can maintain state to collect data across scenarios:
class MetricsPlugin : BerryCrushPlugin {
override val name = "metrics"
override val priority = -100 // Run early (lower = earlier)
private val metrics = mutableMapOf<String, Long>()
override fun onScenarioStart(context: ScenarioContext) {
context.variables["_startTime"] = System.currentTimeMillis()
}
override fun onScenarioEnd(context: ScenarioContext, result: ScenarioResult) {
val startTime = context.variables["_startTime"] as Long
metrics[context.scenarioName] = System.currentTimeMillis() - startTime
}
fun getMetrics(): Map<String, Long> = metrics.toMap()
}
Registering Plugins
Via Annotation
The recommended way to register plugins is via the @BerryCrushConfiguration annotation:
@BerryCrushConfiguration(
pluginClasses = [LoggingPlugin::class, MetricsPlugin::class]
)
class MyApiTest
By Name
Some built-in plugins can be registered by name:
@BerryCrushConfiguration(
plugins = [
"report:json:output.json",
"report:junit:test-results.xml"
]
)
Programmatic Registration
For dynamic plugin configuration:
class MyBindings : BerryCrushBindings {
override fun getPlugins(): List<BerryCrushPlugin> {
return listOf(
LoggingPlugin(),
JsonReportPlugin("berrycrush-report.json")
)
}
}
Built-in Plugins
Report Plugins
BerryCrush includes several report plugins:
TextReportPlugin- Human-readable console outputJsonReportPlugin- Machine-parseable JSON formatXmlReportPlugin- Generic XML structureJunitReportPlugin- JUnit XML for CI/CD integration
See Reporting for detailed configuration.
Best Practices
Keep plugins focused: Each plugin should do one thing well
Use priority wisely: Set explicit priorities when order matters
Handle exceptions: Plugins should not crash test execution
Be thread-safe: Plugins may be called from multiple threads
Clean up resources: Use
onScenarioEndto release resources
Example: Retry Plugin
Here’s a more complex example - a plugin that retries failed steps:
class RetryPlugin(
private val maxRetries: Int = 3,
private val retryDelay: Duration = Duration.ofSeconds(1)
) : BerryCrushPlugin {
override val name = "retry"
override val priority = -100 // Run last
private val retryCount = ThreadLocal.withInitial { 0 }
override fun onStepEnd(context: StepContext, result: StepResult) {
if (result.status == ResultStatus.FAILED && retryCount.get() < maxRetries) {
retryCount.set(retryCount.get() + 1)
Thread.sleep(retryDelay.toMillis())
throw RetryStepException()
}
retryCount.set(0)
}
}