Creating a KTestify Plugin
This guide walks you through creating a ktestify plugin from start to finish using the ktestify-plugin-archetype Maven archetype. The archetype generates a complete, compilable project with all the boilerplate β plugin SPI class, config reader, transport layer, Cucumber step definitions, services, unit tests, and build configuration.
The archetype is the recommended way to start a new plugin. It ensures your project follows the correct structure, naming conventions, and build setup from day one.
How the Archetype Worksβ
The ktestify plugin archetype is a standard Maven archetype that uses Velocity templating to generate a project tailored to your plugin. When you run mvn archetype:generate, Maven prompts you for a set of parameters (plugin name, author, GitHub repo, etc.) and produces a complete project directory.
What gets generatedβ
ktestify-plugin-myplugin/
βββ pom.xml β Full POM with Spotless, JaCoCo, release profile
βββ README.md β Plugin README with step examples
βββ LICENSE β Apache 2.0
βββ cliff.toml β git-cliff changelog config
βββ .gitignore
βββ .spotless/
β βββ HEADER.txt β Apache 2.0 license header (with your name)
βββ src/
βββ main/
β βββ java/.../
β β βββ MyPluginPlugin.java β KtestifyPlugin SPI implementation
β β βββ config/
β β β βββ MyPluginConfig.java β Typed HOCON config reader
β β βββ io/
β β β βββ MyPluginRecordFetcher.java β RecordFetcher<String> (transport layer)
β β β βββ MyPluginConsumer.java β Orchestration (fetch β match β result)
β β β βββ MyPluginConsumerContext.java β Immutable per-fetch context
β β βββ entities/
β β β βββ KtestifyMyPluginEntity.java β Resource entity
β β βββ services/
β β β βββ MyPluginActionService.java β Send/upload service
β β β βββ MyPluginValidationService.java β Validation + timeout orchestration
β β βββ steps/
β β βββ MyPluginBackgroundSteps.java β @Given steps
β β βββ MyPluginActionSteps.java β @When steps
β β βββ MyPluginValidationSteps.java β @Then / @And steps
β β βββ SharedMyPluginResources.java β PicoContainer shared state
β βββ resources/
β βββ reference.conf β Default HOCON config
β βββ log4j2.properties β Logging config
β βββ META-INF/services/
β βββ io.github.ktestify.plugin.KtestifyPlugin
βββ test/
βββ java/.../
βββ MyPluginPluginTest.java β Plugin lifecycle tests
βββ config/
βββ MyPluginConfigTest.java β Config loading tests
Three-layer architectureβ
The generated project follows the same three-layer separation as ktestify-core:
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β TRANSPORT β ORCHESTRATION β ASSERTION β
β MyPluginRecordFetcher β MyPluginConsumerβ RecordMatcher β
β implements RecordFetcher β β (from core) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- Transport β
MyPluginRecordFetcherimplementsRecordFetcher<String>from ktestify-core. ReturnsList<ConsumedRecord<String>>β the only type crossing layer boundaries. - Orchestration β
MyPluginConsumerwires fetch β match β result. UsesRecordMatcherFactory.forRaw()to select the correct matcher. - Assertion β all standard
RecordMatcherimplementations from ktestify-core are reused as-is. No matcher code needed in your plugin.
Phase 1: Generate the Projectβ
Step 1.1 β Run the archetypeβ
mvn archetype:generate \
-DarchetypeGroupId=io.github.ktestify \
-DarchetypeArtifactId=ktestify-plugin-archetype \
-DarchetypeVersion=1.0-SNAPSHOT \
-DgroupId=io.github.ktestify \
-DartifactId=ktestify-plugin-s3 \
-Dversion=1.0-SNAPSHOT \
-Dpackage=io.github.ktestify.s3 \
-DpluginShortName=s3 \
-DpluginKebabId=s3 \
-DpluginPascalName=S3 \
-DauthorName="Your Name" \
-DauthorEmail="your.email@example.com" \
-DgithubOrg=your-github-username \
-DgithubRepoName=ktestify-plugin-s3 \
-DinteractiveMode=false
Step 1.2 β Archetype parametersβ
| Parameter | Description | Example |
|---|---|---|
groupId | Maven groupId for the generated project | io.github.ktestify |
artifactId | Maven artifactId (should start with ktestify-plugin-) | ktestify-plugin-s3 |
version | Initial Maven version | 1.0-SNAPSHOT |
package | Base Java package | io.github.ktestify.s3 |
pluginShortName | Short lowercase name (file names, log files) | s3 |
pluginKebabId | Kebab-case plugin ID (HOCON config paths) | s3 |
pluginPascalName | PascalCase name (Java class names) | S3 |
authorName | Author display name (startup banner + POM) | Your Name |
authorEmail | Author contact email | your.email@example.com |
githubOrg | GitHub username or organisation | your-github-username |
githubRepoName | GitHub repository name | ktestify-plugin-s3 |
ktestify-core version: The generated project depends on the latest released version of
ktestify-core. This version is managed by Dependabot in the archetype repository β when a new version of ktestify-core is released, Dependabot automatically opens a PR to bump it in the template POM.
Step 1.3 β Verify it compilesβ
cd ktestify-plugin-s3
mvn compile
If this succeeds, you have a valid plugin skeleton. The generated code compiles out of the box β all TODO stubs are syntactically valid.
Phase 2: Implement Your Transportβ
The generated code contains TODO markers where you need to add your transport-specific logic. Here's what to implement:
Step 2.1 β Add your transport SDK dependencyβ
Add your transport SDK to pom.xml:
<!-- Example: AWS S3 SDK -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
<version>2.25.0</version>
</dependency>
Step 2.2 β Implement the RecordFetcherβ
Open src/main/java/.../io/S3RecordFetcher.java and implement the fetch() method:
@Override
public List<ConsumedRecord<String>> fetch() throws FetchException {
String recordId = context.getRecordId();
String resourceName = context.getResourceName();
long deadlineMs = System.currentTimeMillis() + resolveReadTimeoutMs();
long pollMs = resolvePollIntervalMs();
while (System.currentTimeMillis() < deadlineMs) {
// TODO: Replace with your SDK call
boolean exists = s3Client.headObject(
HeadObjectRequest.builder()
.bucket(resourceName)
.key(recordId)
.build()
).sdkHttpResponse().isSuccessful();
if (exists) {
return List.of(downloadRecord(resourceName, recordId));
}
sleep(pollMs, recordId);
}
throw new FetchException("Timed out waiting for record '" + recordId + "'");
}
Step 2.3 β Implement the Action Serviceβ
Open src/main/java/.../services/S3ActionService.java and implement the send() method:
public void send(KtestifyS3Entity resource, String recordId, String sourceFile) {
String connStr = resolveConnectionString(resource);
// TODO: Build your client and upload the file
byte[] content = Files.readAllBytes(Path.of(sourceFile));
s3Client.putObject(
PutObjectRequest.builder()
.bucket(resource.getResourceName())
.key(recordId)
.build(),
RequestBody.fromBytes(content)
);
}
Step 2.4 β Customize step wording (optional)β
The generated steps use generic wording like "S3 resource" and "expected S3 record from file". You can rename these to be more natural for your transport:
// Before (generated)
@Given("S3 resource")
// After (customized)
@Given("S3 bucket")
Phase 3: Configurationβ
Step 3.1 β Update reference.confβ
The generated reference.conf has a basic connection-string pattern. Update it to match your transport's authentication model:
ktestify.plugins.s3 {
# AWS credentials (uses DefaultCredentialsProvider if not set)
access-key-id = ""
access-key-id = ${?AWS_ACCESS_KEY_ID}
secret-access-key = ""
secret-access-key = ${?AWS_SECRET_ACCESS_KEY}
region = "us-east-1"
region = ${?AWS_REGION}
endpoint = "" # for LocalStack
endpoint = ${?AWS_ENDPOINT}
read-timeout = 30s
poll-interval = 500ms
}
Step 3.2 β Update the Config classβ
Add fields to S3Config.java to match your new config keys:
private S3Config(Config cfg) {
this.accessKeyId = cfg.getString("access-key-id");
this.secretAccessKey = cfg.getString("secret-access-key");
this.region = cfg.getString("region");
this.endpoint = cfg.getString("endpoint");
this.readTimeoutMs = cfg.getDuration("read-timeout").toMillis();
this.pollIntervalMs = cfg.getDuration("poll-interval").toMillis();
}
Phase 4: Testingβ
Step 4.1 β Unit testsβ
The archetype generates unit tests for the plugin lifecycle and config loading. Run them:
mvn test
Step 4.2 β Integration testsβ
Add integration tests using Testcontainers. Create src/test/java/.../S3PluginIT.java:
@Testcontainers
class S3PluginIT {
@Container
static final GenericContainer<?> localstack =
new GenericContainer<>("localstack/localstack:3.0")
.withEnv("SERVICES", "s3")
.withExposedPorts(4566);
@Test
@DisplayName("upload and validate a blob")
void uploadAndValidate() {
// Point your plugin at LocalStack
// Upload a file via the action service
// Assert it appears via the validation service
}
}
Run integration tests (requires Docker):
mvn verify
Phase 5: Use Your Pluginβ
Option A β As a Maven dependencyβ
<dependency>
<groupId>io.github.ktestify</groupId>
<artifactId>ktestify-plugin-s3</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>test</scope>
</dependency>
Option B β Drop the JAR into /workspace/pluginsβ
docker run --rm \
-v $(pwd)/features:/workspace/features \
-v $(pwd)/plugins:/workspace/plugins \
ghcr.io/ktestify/ktestify-cucumber:latest \
/workspace/features
Example Gherkin scenarioβ
Feature: S3 integration
Background:
Given S3 bucket
| resourceName | resourceAlias |
| test-bucket | blobs |
Scenario: Upload and validate
When S3 record is sent from file
| resourceAlias | file | recordId |
| blobs | payload.json | data.json |
Then expected S3 record from file
| resourceAlias | recordId | file | readTimeout |
| blobs | data.json | expected.json | 30 |
Checklist for Production Qualityβ
Before publishing your plugin, ensure it meets these criteria:
- Plugin implements all methods of
KtestifyPlugininterface - Configuration is loaded from
ktestify.plugins.<id>HOCON subtree -
initialize()validates required config and throwsPluginExceptionon failure -
shutdown()releases all resources (connections, threads, etc.) - Plugin is registered in
META-INF/services/io.github.ktestify.plugin.KtestifyPlugin - Step definitions are in a discoverable package (returned by
getGluePackage()) -
RecordFetcher<V>implementation returnsConsumedRecord<V>correctly -
reference.confdocuments all configuration keys with sensible defaults - Logging uses SLF4J via
@Slf4jannotation - Documentation includes configuration examples and usage scenarios
- README contains a clear description of what the plugin does
- Unit and integration tests pass
- Code is formatted with Maven Spotless (
mvn spotless:apply)
Reference Implementationsβ
Looking for real-world examples? Check out the first-party plugins:
| Plugin | What it demonstrates |
|---|---|
| ktestify-plugin-azureblob | Azure Blob Storage transport with Azurite integration tests |
| ktestify-plugin-notifications | Non-transport plugin: Teams/Slack/webhook notifications with its own SPI |
| ktestify-plugin-skeleton | The reference template the archetype is derived from |
Resourcesβ
- ktestify-plugin-archetype β the archetype repository
- Plugin System Documentation β how the plugin loading works
- ktestify-core API β base classes and interfaces
- Adding a Transport β transport layer deep dive
Next Stepsβ
Once your plugin is complete and tested:
- Publish to Maven Central β follow Sonatype's guide
- Create a GitHub repository β use
ktestify-plugin-*naming convention - Document on docs.ktestify.xyz β add a page next to the Azure Blob example
- Announce in the ktestify community β open a discussion thread
Happy plugin building!