Notifications Plugin
ktestify-plugin-notifications is a first-party KTestify plugin that sends a rich test suite summary at the end of each run. It posts a colour-coded card to Microsoft Teams, Slack, or any HTTP webhook, with per-tag group breakdowns, CI/Git context, configurable success-rate thresholds, and fully user-overridable templates.
This plugin is actively being developed. The API surface, configuration keys, and template variables may evolve before the first stable release.
When to Useβ
Use the Notifications plugin when your team,
- Wants a summary card in Teams or Slack as soon as a test run finishes
- Needs per-domain breakdowns grouped by Cucumber tag (e.g.
@orders,@payments) - Runs tests in CI and wants branch, commit, build number, and pipeline links auto-attached
- Needs to drive alerting based on the suite success rate
- Wants to deliver results to a custom endpoint via a generic webhook or a custom channel
Installationβ
Maven Dependencyβ
<dependency>
<groupId>io.github.ktestify</groupId>
<artifactId>ktestify-plugin-notifications</artifactId>
<version>0.1.1-SNAPSHOT</version>
<scope>test</scope>
</dependency>
With ktestify-cucumber (Docker)β
The Notification plugin is considered as a first-party plugin and is bundled in the ktestify-cucumber fat JAR. No extra steps are required to use it.
Configurationβ
The plugin reads settings from ktestify.plugins.notifications in your HOCON configuration file. Place your configuration in application.conf or override via environment variables.
The plugin is disabled by default. Activate it by setting enabled = true and configuring at least one channel,
ktestify.plugins.notifications {
enabled = true
enabled = ${?KTESTIFY_NOTIFICATIONS_ENABLED}
suite {
name = "My Integration Tests"
environment = ${?KTESTIFY_ENV} # e.g. "staging"
report-url = ${?KTESTIFY_REPORT_URL} # link to Allure / GitLab Pages report
}
channels = [
{
type = "teams"
enabled = true
webhook-url = ${?KTESTIFY_TEAMS_WEBHOOK_URL}
}
]
}
Configuration Priorityβ
- System properties (e.g.
-Dktestify.plugins.notifications.enabled=true) - Environment variables (e.g.
KTESTIFY_NOTIFICATIONS_ENABLED) application.confin classpath- Plugin's
reference.conf(defaults)
Configuration Referenceβ
Top-level Keysβ
| Key | Type | Default | Env var | Description |
|---|---|---|---|---|
enabled | bool | false | KTESTIFY_NOTIFICATIONS_ENABLED | Master on/off switch |
on-failure-only | bool | false | KTESTIFY_NOTIFICATIONS_ON_FAILURE_ONLY | Global default; each channel can override |
suite Blockβ
| Key | Default | Env var | Description |
|---|---|---|---|
name | "ktestify Test Suite" | KTESTIFY_NOTIFICATIONS_SUITE_NAME | Displayed in the notification header |
environment | "" | KTESTIFY_ENV | Environment label (e.g. "staging", "prod") |
report-url | "" | KTESTIFY_REPORT_URL | Link to the published HTML/Allure report |
thresholds Blockβ
The success rate drives the visual styling of the card.
| Key | Default | Description |
|---|---|---|
good | 75 | Success rate % at or above this value uses the "good" style (green) |
warning | 50 | Success rate % at or above this value uses the "warning" style (yellow); below uses "attention" (red) |
groups Listβ
Groups scenarios by Cucumber tag for a per-domain breakdown in the notification card.
groups = [
{ tag = "orders", label = "Order Service", emoji = "π¦" },
{ tag = "payments", label = "Payment Service", emoji = "π³" },
{ tag = "inventory", label = "Inventory", emoji = "π" }
]
Scenarios that match no configured tag are placed in an automatic Untagged group.
channels Listβ
Each entry configures one outbound channel. Common keys,
| Key | Description |
|---|---|
type | Channel type, "teams", "slack", "webhook", or "log" |
enabled | Whether this channel fires |
on-failure-only | Only notify when the suite fails (overrides the global default) |
webhook-url | Incoming webhook URL (Teams / Slack) |
url | Endpoint URL (webhook type) |
method | HTTP method (webhook type, default "POST") |
headers | HTTP headers map (webhook type) |
template | Template source, see Templates |
Channelsβ
Microsoft Teamsβ
{
type = "teams"
enabled = true
webhook-url = ${?KTESTIFY_TEAMS_WEBHOOK_URL}
on-failure-only = false
template = "builtin"
}
The Adaptive Card renders a colour-coded header (green / yellow / red), a fact set with CI/Git metadata, a section per tag group, and action buttons linking to the report and pipeline.
Slackβ
{
type = "slack"
enabled = true
webhook-url = ${?KTESTIFY_SLACK_WEBHOOK_URL}
on-failure-only = true # only notify on failures
template = "builtin"
}
The Block Kit message mirrors the Teams card, a header, a summary section, per-group blocks, and footer links.
Generic HTTP Webhookβ
{
type = "webhook"
enabled = true
url = ${?KTESTIFY_WEBHOOK_URL}
method = "POST"
headers {
Content-Type = "application/json"
Authorization = "Bearer "${?KTESTIFY_WEBHOOK_TOKEN}
}
template = "builtin"
}
Log (debug / local dev)β
{
type = "log"
enabled = true
}
Zero external dependencies. Always available. Writes suite results and per-group breakdowns to the SLF4J logger at INFO.
Templatesβ
The notification payload for each channel is driven by a three-slot template system,
| Slot | Variable | Description |
|---|---|---|
suite | outer card / message body | Rendered once per suite event |
group | per-group section | Rendered once per tag group, injected via {{groupSections}} |
footer | action buttons / links | Rendered once, injected via {{footer}} |
Resolution Order (highest to lowest priority)β
- Inline HOCON
"""..."""string intemplate.suite/template.group/template.footer - Classpath resource,
template = "classpath:my-card.json" - Filesystem path,
template = "/opt/config/teams-card.json" - Bundled default,
template = "builtin"(or omitted)
Inline Template Exampleβ
{
type = "teams"
enabled = true
webhook-url = ${?KTESTIFY_TEAMS_WEBHOOK_URL}
template {
suite = """
{
"type": "AdaptiveCard",
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"version": "1.4",
"body": [
{
"type": "TextBlock",
"text": "π§ͺ **{{suiteName}}**, {{environment}}, {{date}}",
"weight": "Bolder",
"size": "Large"
},
{
"type": "FactSet",
"facts": [
{"title": "Branch", "value": "{{gitBranch}}"},
{"title": "Passed", "value": "{{passedCount}}/{{totalCount}}"},
{"title": "Success", "value": "{{successRate}}%"}
]
},
{{groupSections}}{{footer}}
]
}
"""
group = """
{
"type": "Container",
"style": "{{style}}",
"items": [{"type": "TextBlock", "text": "{{emoji}} **{{groupLabel}}**, {{successRate}}%"}]
}
"""
footer = """
{
"type": "ActionSet",
"actions": [
{"type": "Action.OpenUrl", "title": "π Report", "url": "{{reportUrl}}"},
{"type": "Action.OpenUrl", "title": "π Pipeline", "url": "{{pipelineUrl}}"}
]
}
"""
}
}
Template Variable Referenceβ
Suite-level variablesβ
| Variable | Example |
|---|---|
{{suiteName}} | "My Integration Tests" |
{{environment}} | "staging" |
{{date}} | "2026-06-06" |
{{status}} | "PASSED" / "FAILED" |
{{overallStyle}} | "good" / "warning" / "attention" |
{{totalCount}} | "42" |
{{passedCount}} | "38" |
{{failedCount}} | "4" |
{{skippedCount}} | "0" |
{{successRate}} | "90" |
{{reportUrl}} | "https://pages.gitlab.io/β¦" |
{{ciName}} | "GitHub Actions" |
{{pipelineUrl}} | "https://github.com/β¦/runs/123" |
{{buildNumber}} | "456" |
{{gitBranch}} | "main" |
{{gitRevision}} | "a1b2c3d" |
{{gitTag}} | "v1.4.0" |
{{groupSections}} | (rendered group fragments, comma-joined) |
{{footer}} | (rendered footer fragment) |
Group-level variables (template.group only)β
| Variable | Example |
|---|---|
{{groupLabel}} | "Order Service" |
{{emoji}} | "π¦" |
{{tag}} | "orders" |
{{successRate}} | "67" |
{{style}} | "warning" |
{{passedCount}} | "8" |
{{failedCount}} | "4" |
{{totalCount}} | "12" |
Complete Configuration Exampleβ
ktestify.plugins.notifications {
enabled = true
suite {
name = "Order Pipeline Tests"
environment = "staging"
report-url = ${?KTESTIFY_REPORT_URL}
}
thresholds {
good = 80
warning = 60
}
groups = [
{ tag = "orders", label = "Order Service", emoji = "π¦" },
{ tag = "payments", label = "Payment Service", emoji = "π³" }
]
channels = [
{
type = "teams"
enabled = true
webhook-url = ${?KTESTIFY_TEAMS_WEBHOOK_URL}
},
{
type = "slack"
enabled = true
webhook-url = ${?KTESTIFY_SLACK_WEBHOOK_URL}
on-failure-only = true
},
{
type = "log"
enabled = true
}
]
}
CI/CD Auto-detectionβ
Branch, commit, build number, and pipeline link are automatically extracted from the running environment, no manual configuration required. The plugin detects GitLab CI, GitHub Actions, CircleCI, Jenkins, and others via cucumber-cienvironment, then exposes the values through the {{ciName}}, {{pipelineUrl}}, {{buildNumber}}, {{gitBranch}}, {{gitRevision}}, and {{gitTag}} template variables.
When running outside a recognised CI system, these variables fall back to empty strings and the card still renders cleanly.
Implementation Structureβ
The plugin follows the layered architecture defined by ktestify-core,
ktestify-plugin-notifications/
βββ src/main/java/io/github/ktestify/notifications/
βββ NotificationsPlugin.java β KtestifyPlugin SPI impl
βββ config/
β βββ NotificationsConfig.java β reads ktestify.plugins.notifications HOCON
β βββ SuiteConfig.java β suite block model
β βββ ThresholdsConfig.java β success-rate thresholds
β βββ TagGroupConfig.java β tag group definitions
β βββ ChannelConfig.java β per-channel settings
βββ hooks/
β βββ NotificationHooks.java β @Before / @After / @AfterAll glue
βββ model/
β βββ ScenarioEvent.java β single scenario result
β βββ SuiteEvent.java β aggregated suite result
β βββ GroupResult.java β per-tag breakdown
β βββ CiContext.java β CI metadata
β βββ GitContext.java β Git metadata
β βββ NotificationStatus.java β PASSED / FAILED enum
βββ service/
β βββ ScenarioAggregator.java β collects scenarios, builds SuiteEvent
β βββ CiContextResolver.java β resolves CI/Git context
β βββ NotificationService.java β async dispatch + shutdown
βββ channel/
β βββ NotificationChannel.java β channel SPI
β βββ NotificationChannelFactory.java β builds channels from config
β βββ impl/
β βββ LogNotificationChannel.java
β βββ TeamsNotificationChannel.java
β βββ SlackNotificationChannel.java
β βββ WebhookNotificationChannel.java
βββ template/
βββ NotificationTemplateEngine.java β renders suite/group/footer
βββ NotificationTemplateResolver.java β 4-level template lookup
βββ BuiltinTemplates.java β bundled fallback templates
Execution flow :
NotificationHooks (@Before / @After / @AfterAll)
β
ββ @Before β records scenario start time
β
ββ @After β ScenarioEvent.from(scenario, durationMs)
β ββ ScenarioAggregator.record(event)
β
ββ @AfterAll β CiContextResolver.resolve()
ScenarioAggregator.buildSuiteEvent(config, ci, git)
ββ tag grouping + success-rate + style computation
NotificationService.dispatch(suiteEvent)
ββ CompletableFuture.runAsync() per channel
NotificationService.shutdown(30s)
Key layers :
- Aggregation,
ScenarioAggregatorcollects each scenario result and builds the finalSuiteEvent - Resolution,
CiContextResolverextracts CI and Git metadata for the card - Dispatch,
NotificationServicefans out the event to each channel asynchronously - Rendering,
NotificationTemplateEngineresolves and fills templates per channel
Custom Channels (SPI)β
Implement NotificationChannel and register it via the Java SPI,
// com.example.MyPagerDutyChannel.java
public class MyPagerDutyChannel implements NotificationChannel {
@Override public String getType() { return "pagerduty"; }
@Override
public void sendSuite(SuiteEvent event) {
// send a PagerDuty alert if event.getStatus() == FAILED
}
}
# META-INF/services/io.github.ktestify.notifications.channel.NotificationChannel
com.example.MyPagerDutyChannel
Then reference it in your config,
channels = [{ type = "pagerduty", enabled = true }]
Custom channels registered in META-INF/services/β¦NotificationChannel are discovered automatically alongside the built-in ones.
Error Handlingβ
The plugin is designed to be failure-safe, a broken webhook must never fail a test run.
Channel Send Failureβ
If a channel throws while sending, the exception is caught, logged at WARN, and swallowed,
WARN io.github.ktestify.notifications.service.NotificationService -
Channel 'teams' failed to send suite notification: 404 Not Found
The remaining channels still fire, and the test result is unaffected.
No Channel Configuredβ
If enabled = true but the channels list is empty (or every channel is disabled), nothing is sent and a single diagnostic line is logged. Add at least one enabled channel to receive notifications.
Notifications Not Arrivingβ
Check that,
enabled = trueis set (the plugin is off by default)- At least one channel has
enabled = true - The
webhook-url(orurl) resolves to a non-empty value on-failure-onlyis not suppressing a passing run
Advanced Topicsβ
Failure-only Notificationsβ
Set on-failure-only = true globally, or per channel, to only notify when the suite fails,
ktestify.plugins.notifications {
enabled = true
on-failure-only = true # global default
channels = [
{ type = "teams", enabled = true, webhook-url = ${?KTESTIFY_TEAMS_WEBHOOK_URL} },
{ type = "log", enabled = true, on-failure-only = false } # always log
]
}
Threshold-driven Stylingβ
Tune thresholds.good and thresholds.warning to match your team's quality bar. The computed style (good, warning, attention) is exposed as {{overallStyle}} at the suite level and {{style}} per group, so custom templates can react to it.
Key Design Rulesβ
sendSuite()must never throw, exceptions are caught, logged atWARN, and swallowed- Notifications are async, test execution is never blocked by a slow webhook
- The plugin has zero overhead when disabled, no channels are instantiated and no threads are started
Troubleshootingβ
| Issue | Solution |
|---|---|
| Plugin not discovered | Ensure the JAR is on the classpath or in /workspace/plugins, check logs for ServiceLoader errors |
| No notification sent | Verify enabled = true and that at least one channel is enabled |
| Webhook returns 404 / 401 | Verify the webhook-url / url and any auth headers, regenerate the incoming webhook if needed |
| CI fields are empty | Confirm the run is inside a supported CI provider; locally these fields are blank by design |
| Card looks wrong | Validate your inline template JSON, or fall back to template = "builtin" |
Contributingβ
The Notifications plugin is actively maintained. Report issues and contribute improvements at,
ktestify-plugin-notifications on GitHub
See Alsoβ
- Plugin System Overview, how plugins work
- Creating a Plugin, build your own plugin from scratch
- Azure Blob Storage Plugin, another first-party plugin reference